diff --git a/.gitignore b/.gitignore
index a8e65fd..e502041 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,5 @@ out/
*.tar
.direnv/
-.envrc
\ No newline at end of file
+.envrc
+localjson
diff --git a/Dockerfile b/Dockerfile
index 107bf08..9659fbe 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM --platform=linux/amd64 openjdk:21
+FROM maven:3.9.9-amazoncorretto-21-debian AS build
WORKDIR /app
@@ -6,4 +6,4 @@ COPY ./build/libs/ilp-coursework-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
-ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
index 2d0ff96..93b23fa 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
@@ -20,6 +20,10 @@ body:json {
"capacity": 0.75,
"heating": true,
"maxCost": 13.5
+ },
+ "delivery": {
+ "lng": -3.00,
+ "lat": 55.121
}
},
{
@@ -30,6 +34,10 @@ body:json {
"capacity": 0.75,
"heating": true,
"maxCost": 13.5
+ },
+ "delivery": {
+ "lng": -3.00,
+ "lat": 55.121
}
}
]
diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Example.bru b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
index 9de1eaa..9fed8e5 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
@@ -21,6 +21,10 @@ body:json {
"cooling": false,
"heating": true,
"maxCost": 13.5
+ },
+ "delivery": {
+ "lng": -3.00,
+ "lat": 55.121
}
}
]
diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
index 7fd90ff..c629adc 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
@@ -20,7 +20,8 @@ body:json {
"capacity": 0.75,
"heating": true,
"maxCost": 13.5
- }
+ },
+ "delivery": null
}
]
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java b/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
index 3b73161..05be9ea 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
@@ -1,18 +1,17 @@
package io.github.js0ny.ilp_coursework.controller;
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import io.github.js0ny.ilp_coursework.data.common.Region;
+import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
+import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
+import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
+import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
-import io.github.js0ny.ilp_coursework.data.common.LngLat;
-import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
-import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
-import io.github.js0ny.ilp_coursework.data.common.Region;
-import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
-
/**
* Main REST Controller for the ILP Coursework 1 application.
*
@@ -53,7 +52,6 @@ public class ApiController {
*/
@PostMapping("/distanceTo")
public double getDistance(@RequestBody DistanceRequest request) {
-
LngLat position1 = request.position1();
LngLat position2 = request.position2();
return gpsService.calculateDistance(position1, position2);
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java b/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java
index ee9a8dd..ee23eea 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/controller/DroneController.java
@@ -1,11 +1,12 @@
package io.github.js0ny.ilp_coursework.controller;
-import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import io.github.js0ny.ilp_coursework.data.external.Drone;
-import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
-import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
+import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
+import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
+import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
+import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@@ -21,8 +22,8 @@ import org.springframework.web.client.RestTemplate;
@RequestMapping("/api/v1")
public class DroneController {
- private final DroneInfoService droneService;
- private final RestTemplate restTemplate = new RestTemplate();
+ private final DroneInfoService droneInfoService;
+ private final DroneAttrComparatorService droneAttrComparatorService;
/**
* Constructor of the {@code DroneController} with the business logic dependency
@@ -34,8 +35,12 @@ public class DroneController {
*
* @param droneService The service component that contains all business logic
*/
- public DroneController(DroneInfoService droneService) {
- this.droneService = droneService;
+ public DroneController(
+ DroneInfoService droneService,
+ DroneAttrComparatorService droneAttrComparatorService
+ ) {
+ this.droneInfoService = droneService;
+ this.droneAttrComparatorService = droneAttrComparatorService;
}
/**
@@ -47,8 +52,10 @@ public class DroneController {
* @return An array of drone id with cooling capability.
*/
@GetMapping("/dronesWithCooling/{state}")
- public String[] getDronesWithCoolingCapability(@PathVariable boolean state) {
- return droneService.dronesWithCooling(state);
+ public List getDronesWithCoolingCapability(
+ @PathVariable boolean state
+ ) {
+ return droneInfoService.dronesWithCooling(state);
}
/**
@@ -61,7 +68,7 @@ public class DroneController {
@GetMapping("/droneDetails/{id}")
public ResponseEntity getDroneDetail(@PathVariable String id) {
try {
- Drone drone = droneService.droneDetail(id);
+ Drone drone = droneInfoService.droneDetail(id);
return ResponseEntity.ok(drone);
} catch (IllegalArgumentException ex) {
return ResponseEntity.notFound().build();
@@ -77,30 +84,44 @@ public class DroneController {
* @return An array of drone id that matches the attribute name and value
*/
@GetMapping("/queryAsPath/{attrName}/{attrVal}")
- public String[] getIdByAttrMap(
- @PathVariable String attrName,
- @PathVariable String attrVal) {
- return droneService.dronesWithAttribute(attrName, attrVal);
+ public List getIdByAttrMap(
+ @PathVariable String attrName,
+ @PathVariable String attrVal
+ ) {
+ return droneAttrComparatorService.dronesWithAttribute(
+ attrName,
+ attrVal
+ );
}
@PostMapping("/query")
- public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
- return droneService.dronesSatisfyingAttributes(attrComparators);
+ public List getIdByAttrMapPost(
+ @RequestBody AttrQueryRequest[] attrComparators
+ ) {
+ return droneAttrComparatorService.dronesSatisfyingAttributes(
+ attrComparators
+ );
}
@PostMapping("/queryAvailableDrones")
- public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
- return droneService.dronesMatchesRequirements(records);
+ public List queryAvailableDrones(
+ @RequestBody MedDispatchRecRequest[] records
+ ) {
+ return droneInfoService.dronesMatchesRequirements(records);
}
@PostMapping("/calcDeliveryPath")
- public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
- return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{});
+ public DeliveryPathResponse calculateDeliveryPath(
+ @RequestBody MedDispatchRecRequest[] record
+ ) {
+ // return new DeliveryPathResponse(0.0f, 0, new DronePathDto[] {});
+ return null;
}
@PostMapping("/calcDeliveryPathAsGeoJson")
- public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
+ public String calculateDeliveryPathAsGeoJson(
+ @RequestBody MedDispatchRecRequest[] record
+ ) {
return "{}";
}
-
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/AltitudeRange.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/AltitudeRange.java
new file mode 100644
index 0000000..519a371
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/AltitudeRange.java
@@ -0,0 +1,14 @@
+package io.github.js0ny.ilp_coursework.data.common;
+
+import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
+
+/**
+ * Represents a range of altitude values (that is a fly-zone in {@link RestrictedArea}).
+ *
+ * @param lower The lower bound of the altitude range.
+ * @param upper The upper bound of the altitude range. If {@code upper = -1}, then the region
+ * is not a fly zone.
+ *
+ */
+public record AltitudeRange(double lower, double upper) {
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DronePathDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/DronePathDto.java
deleted file mode 100644
index 88aa359..0000000
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DronePathDto.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package io.github.js0ny.ilp_coursework.data.common;
-
-public record DronePathDto() {
-}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
index b239d8e..9a4ee50 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
@@ -8,4 +8,7 @@ package io.github.js0ny.ilp_coursework.data.common;
* @param lat latitude of the coordinate/point
*/
public record LngLat(double lng, double lat) {
+ public LngLat(LngLatAlt coord) {
+ this(coord.lng(), coord.lat());
+ }
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLatAlt.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLatAlt.java
new file mode 100644
index 0000000..6d4544c
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLatAlt.java
@@ -0,0 +1,12 @@
+package io.github.js0ny.ilp_coursework.data.common;
+
+/**
+ * Represents the data transfer object for a point or coordinate
+ * that defines by a longitude and latitude
+ *
+ * @param lng longitude of the coordinate/point
+ * @param lat latitude of the coordinate/point
+ * @param alt altitude of the coordinate/point
+ */
+public record LngLatAlt(double lng, double lat, double alt) {
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/MedRequirement.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/MedRequirement.java
deleted file mode 100644
index b06e190..0000000
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/MedRequirement.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.github.js0ny.ilp_coursework.data.common;
-
-public record MedRequirement(
- float capacity,
- boolean cooling,
- boolean heating,
- float maxCost
-) {
-}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
index c491c55..740a1d1 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
@@ -1,7 +1,8 @@
package io.github.js0ny.ilp_coursework.data.common;
+import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
-
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -47,5 +48,4 @@ public record Region(String name, List vertices) {
LngLat last = vertices.getLast();
return Objects.equals(last, first);
}
-
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java b/src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
new file mode 100644
index 0000000..c9e9e10
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
@@ -0,0 +1,23 @@
+package io.github.js0ny.ilp_coursework.data.external;
+
+import io.github.js0ny.ilp_coursework.data.common.AltitudeRange;
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
+import io.github.js0ny.ilp_coursework.data.common.Region;
+import java.util.ArrayList;
+import java.util.List;
+
+public record RestrictedArea(
+ String name,
+ int id,
+ AltitudeRange limits,
+ LngLatAlt[] vertices
+) {
+ public Region toRegion() {
+ List vertices2D = new ArrayList<>();
+ for (var vertex : vertices) {
+ vertices2D.add(new LngLat(vertex.lng(), vertex.lat()));
+ }
+ return new Region(name, vertices2D);
+ }
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java b/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
new file mode 100644
index 0000000..955fec3
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
@@ -0,0 +1,5 @@
+package io.github.js0ny.ilp_coursework.data.external;
+
+import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
+
+public record ServicePoint(String name, int id, LngLatAlt location) {}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePointDrones.java b/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePointDrones.java
index 9c17fce..8f5cbd8 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePointDrones.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePointDrones.java
@@ -4,7 +4,7 @@ import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
import org.springframework.lang.Nullable;
public record ServicePointDrones(
- String servicePointId,
+ int servicePointId,
DroneAvailability[] drones) {
@Nullable
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/request/MedDispatchRecRequest.java b/src/main/java/io/github/js0ny/ilp_coursework/data/request/MedDispatchRecRequest.java
index d361464..57ee11f 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/request/MedDispatchRecRequest.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/request/MedDispatchRecRequest.java
@@ -1,15 +1,24 @@
package io.github.js0ny.ilp_coursework.data.request;
-import io.github.js0ny.ilp_coursework.data.common.MedRequirement;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import java.time.LocalDate;
import java.time.LocalTime;
+@JsonIgnoreProperties(ignoreUnknown = true)
public record MedDispatchRecRequest(
int id,
LocalDate date,
LocalTime time,
MedRequirement requirements,
LngLat delivery) {
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record MedRequirement(
+ float capacity,
+ boolean cooling,
+ boolean heating,
+ float maxCost
+ ) {
+ }
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/response/DeliveryPathResponse.java b/src/main/java/io/github/js0ny/ilp_coursework/data/response/DeliveryPathResponse.java
index 5270863..e029594 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/response/DeliveryPathResponse.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/response/DeliveryPathResponse.java
@@ -1,9 +1,14 @@
package io.github.js0ny.ilp_coursework.data.response;
-import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import java.util.List;
public record DeliveryPathResponse(
- float totalCost,
- int totalMoves,
- DronePathDto[] dronePaths) {
+ float totalCost,
+ int totalMoves,
+ DronePath[] dronePaths
+) {
+ public record DronePath(int droneId, List deliveries) {
+ public record Delivery(int deliveryId, List flightPath) {}
+ }
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java
new file mode 100644
index 0000000..94f0061
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java
@@ -0,0 +1,140 @@
+package io.github.js0ny.ilp_coursework.service;
+
+import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.js0ny.ilp_coursework.data.external.Drone;
+import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
+import io.github.js0ny.ilp_coursework.util.AttrOperator;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Service
+public class DroneAttrComparatorService {
+
+ private final String baseUrl;
+ private final String dronesEndpoint = "drones";
+ private final RestTemplate restTemplate = new RestTemplate();
+
+ /**
+ * Constructor, handles the base url here.
+ */
+ public DroneAttrComparatorService() {
+ String baseUrl = System.getenv("ILP_ENDPOINT");
+ if (baseUrl == null || baseUrl.isBlank()) {
+ this.baseUrl =
+ "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
+ } else {
+ // Defensive: Add '/' to the end of the URL
+ if (!baseUrl.endsWith("/")) {
+ baseUrl += "/";
+ }
+ this.baseUrl = baseUrl;
+ }
+ }
+
+ /**
+ * Return an array of ids of drones with a given attribute name and value.
+ *
+ * Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
+ *
+ * @param attrName the attribute name to filter on
+ * @param attrVal the attribute value to filter on
+ * @return array of drone ids matching the attribute name and value
+ * @see #dronesWithAttributeCompared(String, String, AttrOperator)
+ * @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
+ */
+ public List dronesWithAttribute(String attrName, String attrVal) {
+ // Call the helper with EQ operator
+ return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
+ }
+
+ /**
+ * Return an array of ids of drones which matches all given complex comparing
+ * rules
+ *
+ * @param attrComparators The filter rule with Name, Value and Operator
+ * @return array of drone ids that matches all rules
+ */
+ public List dronesSatisfyingAttributes(
+ AttrQueryRequest[] attrComparators
+ ) {
+ Set matchingDroneIds = null;
+ for (var comparator : attrComparators) {
+ String attribute = comparator.attribute();
+ String operator = comparator.operator();
+ String value = comparator.value();
+ AttrOperator op = AttrOperator.fromString(operator);
+ List ids = dronesWithAttributeCompared(
+ attribute,
+ value,
+ op
+ );
+ if (matchingDroneIds == null) {
+ matchingDroneIds = new HashSet<>(ids);
+ } else {
+ matchingDroneIds.retainAll(ids);
+ }
+ }
+ if (matchingDroneIds == null) {
+ return new ArrayList<>();
+ }
+ return matchingDroneIds.stream().toList();
+ }
+
+ /**
+ * Helper that wraps the dynamic querying with different comparison operators
+ *
+ * This method act as a concatenation of
+ * {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
+ * AttrOperator)}
+ *
+ * @param attrName the attribute name to filter on
+ * @param attrVal the attribute value to filter on
+ * @param op the comparison operator
+ * @return array of drone ids matching the attribute name and value (filtered by
+ * {@code op})
+ * @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
+ * String,
+ * AttrOperator)
+ */
+ private List dronesWithAttributeCompared(
+ String attrName,
+ String attrVal,
+ AttrOperator op
+ ) {
+ URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
+ // This is required to make sure the response is valid
+ Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
+
+ if (drones == null) {
+ return new ArrayList<>();
+ }
+
+ // Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
+ // querying
+ ObjectMapper mapper = new ObjectMapper();
+
+ return Arrays.stream(drones)
+ .filter(drone -> {
+ JsonNode node = mapper.valueToTree(drone);
+ JsonNode attrNode = node.findValue(attrName);
+ if (attrNode != null) {
+ // Manually handle different types of JsonNode
+ return isValueMatched(attrNode, attrVal, op);
+ } else {
+ return false;
+ }
+ })
+ .map(Drone::id)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
index b764552..6ecd613 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
@@ -1,33 +1,38 @@
package io.github.js0ny.ilp_coursework.service;
-import io.github.js0ny.ilp_coursework.data.external.Drone;
-import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
-import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
-import io.github.js0ny.ilp_coursework.util.AttrOperator;
-import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
-
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import io.github.js0ny.ilp_coursework.data.external.Drone;
+import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
+import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
+import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
+import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
+import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
+import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
+import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
+import io.github.js0ny.ilp_coursework.util.AttrOperator;
import java.net.URI;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
+import java.util.*;
+import java.util.stream.Collectors;
+import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
@Service
public class DroneInfoService {
private final String baseUrl;
private final String dronesEndpoint = "drones";
- private final String dronesForServicePointsEndpoint = "drones-for-service-points";
+ private final String dronesForServicePointsEndpoint =
+ "drones-for-service-points";
+ public static final String servicePointsEndpoint = "service-points";
+ public static final String restrictedAreasEndpoint = "restricted-areas";
private final RestTemplate restTemplate = new RestTemplate();
@@ -37,7 +42,8 @@ public class DroneInfoService {
public DroneInfoService() {
String baseUrl = System.getenv("ILP_ENDPOINT");
if (baseUrl == null || baseUrl.isBlank()) {
- this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
+ this.baseUrl =
+ "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
} else {
// Defensive: Add '/' to the end of the URL
if (!baseUrl.endsWith("/")) {
@@ -57,20 +63,20 @@ public class DroneInfoService {
* capability, else without cooling
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
*/
- public String[] dronesWithCooling(boolean state) {
- URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
- Drone[] drones = restTemplate.getForObject(
- droneUrl,
- Drone[].class);
+ public List dronesWithCooling(boolean state) {
+ // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
+ // Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
+ List drones = fetchAllDrones();
if (drones == null) {
- return new String[]{};
+ return new ArrayList<>();
}
- return Arrays.stream(drones)
- .filter(drone -> drone.capability().cooling() == state)
- .map(Drone::id)
- .toArray(String[]::new);
+ return drones
+ .stream()
+ .filter(drone -> drone.capability().cooling() == state)
+ .map(Drone::id)
+ .collect(Collectors.toList());
}
/**
@@ -88,10 +94,7 @@ public class DroneInfoService {
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
*/
public Drone droneDetail(String id) {
- URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
- Drone[] drones = restTemplate.getForObject(
- droneUrl,
- Drone[].class);
+ List drones = fetchAllDrones();
if (drones == null) {
throw new NullPointerException("drone cannot be found");
@@ -105,96 +108,8 @@ public class DroneInfoService {
// This will result in 404
throw new IllegalArgumentException(
- "drone with that ID cannot be found");
- }
-
- /**
- * Return an array of ids of drones with a given attribute name and value.
- *
- * Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
- *
- * @param attrName the attribute name to filter on
- * @param attrVal the attribute value to filter on
- * @return array of drone ids matching the attribute name and value
- * @see #dronesWithAttributeCompared(String, String, AttrOperator)
- * @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
- */
- public String[] dronesWithAttribute(String attrName, String attrVal) {
- // Call the helper with EQ operator
- return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
- }
-
- /**
- * Return an array of ids of drones which matches all given complex comparing
- * rules
- *
- * @param attrComparators The filter rule with Name, Value and Operator
- * @return array of drone ids that matches all rules
- */
- public String[] dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
- Set matchingDroneIds = null;
- for (var comparator : attrComparators) {
- String attribute = comparator.attribute();
- String operator = comparator.operator();
- String value = comparator.value();
- AttrOperator op = AttrOperator.fromString(operator);
- String[] ids = dronesWithAttributeCompared(attribute, value, op);
- if (matchingDroneIds == null) {
- matchingDroneIds = new HashSet<>(Arrays.asList(ids));
- } else {
- matchingDroneIds.retainAll(Arrays.asList(ids));
- }
- }
- if (matchingDroneIds == null) {
- return new String[]{};
- }
- return matchingDroneIds.toArray(String[]::new);
- }
-
- /**
- * Helper that wraps the dynamic querying with different comparison operators
- *
- * This method act as a concatenation of
- * {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
- * AttrOperator)}
- *
- * @param attrName the attribute name to filter on
- * @param attrVal the attribute value to filter on
- * @param op the comparison operator
- * @return array of drone ids matching the attribute name and value (filtered by
- * {@code op})
- * @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
- * String,
- * AttrOperator)
- */
- private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) {
- URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
- // This is required to make sure the response is valid
- Drone[] drones = restTemplate.getForObject(
- droneUrl,
- Drone[].class);
-
- if (drones == null) {
- return new String[]{};
- }
-
- // Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
- // querying
- ObjectMapper mapper = new ObjectMapper();
-
- return Arrays.stream(drones)
- .filter(drone -> {
- JsonNode node = mapper.valueToTree(drone);
- JsonNode attrNode = node.findValue(attrName);
- if (attrNode != null) {
- // Manually handle different types of JsonNode
- return isValueMatched(attrNode, attrVal, op);
- } else {
- return false;
- }
- })
- .map(Drone::id)
- .toArray(String[]::new);
+ "drone with that ID cannot be found"
+ );
}
/**
@@ -207,27 +122,34 @@ public class DroneInfoService {
* @return array of drone ids that match all the requirements
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
*/
- public String[] dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
- URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
- Drone[] drones = restTemplate.getForObject(
- droneUrl,
- Drone[].class);
+ public List dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
+ List drones = fetchAllDrones();
- if (drones == null || rec == null || rec.length == 0) {
- return new String[]{};
+ if (drones == null) {
+ return new ArrayList<>();
+ }
+
+ if (rec == null || rec.length == 0) {
+ return drones
+ .stream()
+ .filter(Objects::nonNull)
+ .map(Drone::id)
+ .collect(Collectors.toList());
}
/*
* Traverse and filter drones, pass every record's requirement to helper
*/
- return Arrays.stream(drones)
- .filter(drone -> drone != null && drone.capability() != null)
- .filter(drone -> Arrays.stream(rec)
- .filter(record -> record != null && record.requirements() != null)
- // Every record must be met
- .allMatch(record -> meetsRequirement(drone, record)))
- .map(Drone::id)
- .toArray(String[]::new);
+ return drones
+ .stream()
+ .filter(d -> d != null && d.capability() != null)
+ .filter(d ->
+ Arrays.stream(rec)
+ .filter(r -> r != null && r.requirements() != null)
+ .allMatch(r -> meetsRequirement(d, r))
+ )
+ .map(Drone::id)
+ .collect(Collectors.toList());
}
/**
@@ -240,14 +162,16 @@ public class DroneInfoService {
* is invalid (capacity and id cannot be null
* in {@code MedDispathRecDto})
*/
- private boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
+ public boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
var requirements = record.requirements();
if (requirements == null) {
throw new IllegalArgumentException("requirements cannot be null");
}
var capability = drone.capability();
if (capability == null) {
- throw new IllegalArgumentException("drone capability cannot be null");
+ throw new IllegalArgumentException(
+ "drone capability cannot be null"
+ );
}
float requiredCapacity = requirements.capacity();
@@ -256,24 +180,24 @@ public class DroneInfoService {
}
// Use boolean wrapper to allow null (not specified) values
- Boolean requiredCooling = requirements.cooling();
- Boolean requiredHeating = requirements.heating();
- Float requiredMaxCost = requirements.maxCost();
+ boolean requiredCooling = requirements.cooling();
+ boolean requiredHeating = requirements.heating();
- boolean matchesCooling = requiredCooling == null || capability.cooling() == requiredCooling;
- boolean matchesHeating = requiredHeating == null || capability.heating() == requiredHeating;
- boolean matchesCost = false;
-
- float totalCost = capability.costInitial() + capability.costFinal();
-
- if (capability.maxMoves() > 0) {
- totalCost += capability.costPerMove();
- }
- matchesCost = totalCost <= requiredMaxCost;
+ // Case 1: required is null: We don't care about it
+ // Case 2: required is false: We don't care about it (high capability adapts to low requirements)
+ // Case 3: capability is true: Then always matches
+ // See: https://piazza.com/class/me9vp64lfgf4sn/post/100
+ boolean matchesCooling = !requiredCooling || capability.cooling();
+ boolean matchesHeating = !requiredHeating || capability.heating();
// Conditions: All requirements matched + availability matched, use helper
// For minimal privilege, only pass drone id to check availability
- return matchesCooling && matchesHeating && matchesCost && checkAvailability(drone.id(), record);
+ return (
+ matchesCooling &&
+ matchesHeating &&
+ checkAvailability(drone.id(), record)
+ ); // &&
+ // checkCost(drone, record) // checkCost is more expensive than checkAvailability
}
/**
@@ -284,25 +208,213 @@ public class DroneInfoService {
* time
* @return true if the drone is available, false otherwise
*/
- private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
- URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
+ private boolean checkAvailability(
+ String droneId,
+ MedDispatchRecRequest record
+ ) {
+ URI droneUrl = URI.create(baseUrl).resolve(
+ dronesForServicePointsEndpoint
+ );
ServicePointDrones[] servicePoints = restTemplate.getForObject(
- droneUrl,
- ServicePointDrones[].class);
+ droneUrl,
+ ServicePointDrones[].class
+ );
LocalDate requiredDate = record.date();
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
LocalTime requiredTime = record.time();
+ assert servicePoints != null;
for (var servicePoint : servicePoints) {
var drone = servicePoint.locateDroneById(droneId); // Nullable
if (drone != null) {
return drone.checkAvailability(requiredDay, requiredTime);
}
-
}
return false;
+ }
+ private LngLat queryServicePointLocationByDroneId(String droneId) {
+ URI droneUrl = URI.create(baseUrl).resolve(
+ dronesForServicePointsEndpoint
+ );
+ ServicePointDrones[] servicePoints = restTemplate.getForObject(
+ droneUrl,
+ ServicePointDrones[].class
+ );
+
+ assert servicePoints != null;
+ for (var sp : servicePoints) {
+ var drone = sp.locateDroneById(droneId); // Nullable
+ if (drone != null) {
+ return queryServicePointLocation(sp.servicePointId());
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private LngLat queryServicePointLocation(int id) {
+ URI servicePointUrl = URI.create(baseUrl).resolve(
+ servicePointsEndpoint
+ );
+
+ ServicePoint[] servicePoints = restTemplate.getForObject(
+ servicePointUrl,
+ ServicePoint[].class
+ );
+
+ assert servicePoints != null;
+ for (var sp : servicePoints) {
+ if (sp.id() == id) {
+ // We dont consider altitude
+ return new LngLat(sp.location());
+ }
+ }
+ return null;
+ }
+
+ // private Set parseObstacles() {
+ // URI restrictedAreasUrl = URI.create(baseUrl).resolve(
+ // restrictedAreasEndpoint
+ // );
+ //
+ // RestrictedArea[] restrictedAreas = restTemplate.getForObject(
+ // restrictedAreasUrl,
+ // RestrictedArea[].class
+ // );
+ //
+ // assert restrictedAreas != null;
+ // Set obstacles = new HashSet<>();
+ // for (var ra : restrictedAreas) {
+ // obstacles.add(new LngLat(ra.location()));
+ // }
+ // return obstacles;
+ // }
+
+ // public DeliveryPathResponse calcDeliveryPath(
+ // MedDispatchRecRequest[] records
+ // ) {
+ // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
+ // Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
+ // List restrictedAreas = fetchRestrictedAreas();
+ // List totalPath = new ArrayList<>();
+ // List deliveries = new ArrayList<>();
+
+ // int moves = 0;
+ // float cost = 0;
+ // for (var record : records) {
+ // assert drones != null;
+ // Drone[] possibleDrones = Arrays.stream(drones)
+ // .filter(d -> meetsRequirement(d, record))
+ // .toArray(Drone[]::new);
+ // int shortestPathCount = Integer.MAX_VALUE;
+ // float lowestCost = Float.MAX_VALUE;
+ // List shortestPath = null;
+ // for (var d : possibleDrones) {
+ // var start = queryServicePointLocationByDroneId(d.id());
+ // List path = PathFinderService.findPath(
+ // start,
+ // record.delivery(),
+ // restrictedAreas
+ // );
+ // float pathCost = path.size() * d.capability().costPerMove();
+ // if (
+ // path.size() < d.capability().maxMoves() &&
+ // pathCost < lowestCost
+ // ) {
+ // shortestPathCount = path.size();
+ // lowestCost = pathCost;
+ // shortestPath = path;
+ // }
+ // }
+ // // deliveries.add(new Delivery(record.id(), shortestPath));
+ // }
+ // // return new
+ // }
+
+ private List fetchAllDrones() {
+ URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
+ Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
+ return Arrays.asList(drones);
+ }
+
+ private List fetchRestrictedAreas() {
+ URI restrictedUrl = URI.create(baseUrl).resolve(
+ restrictedAreasEndpoint
+ );
+ RestrictedArea[] restrictedAreas = restTemplate.getForObject(
+ restrictedUrl,
+ RestrictedArea[].class
+ );
+ assert restrictedAreas != null;
+ List restrictedAreaList = Arrays.asList(
+ restrictedAreas
+ );
+ return restrictedAreaList;
+ }
+
+ private List fetchServicePoints() {
+ URI servicePointUrl = URI.create(baseUrl).resolve(
+ servicePointsEndpoint
+ );
+ ServicePoint[] servicePoints = restTemplate.getForObject(
+ servicePointUrl,
+ ServicePoint[].class
+ );
+ assert servicePoints != null;
+ List servicePointList = Arrays.asList(servicePoints);
+ return servicePointList;
+ }
+
+ private List fetchDronesForServicePoints() {
+ URI servicePointDronesUrl = URI.create(baseUrl).resolve(
+ dronesForServicePointsEndpoint
+ );
+ ServicePointDrones[] servicePointDrones = restTemplate.getForObject(
+ servicePointDronesUrl,
+ ServicePointDrones[].class
+ );
+ assert servicePointDrones != null;
+ List servicePointDronesList = Arrays.asList(
+ servicePointDrones
+ );
+ return servicePointDronesList;
+ }
+
+ // NOTE: Not used.
+ private boolean checkCost(Drone drone, MedDispatchRecRequest rec) {
+ if (rec.delivery() == null) {
+ return true;
+ }
+ URI droneUrl = URI.create(baseUrl).resolve(
+ dronesForServicePointsEndpoint
+ );
+ ServicePointDrones[] servicePoints = restTemplate.getForObject(
+ droneUrl,
+ ServicePointDrones[].class
+ );
+
+ GpsCalculationService gpsService = new GpsCalculationService();
+
+ double steps = gpsService.calculateSteps(
+ queryServicePointLocationByDroneId(drone.id()),
+ rec.delivery()
+ );
+
+ if (steps > drone.capability().maxMoves()) {
+ return false;
+ }
+
+ float baseCost =
+ drone.capability().costInitial() + drone.capability().costFinal();
+
+ double cost = baseCost + drone.capability().costPerMove() * steps;
+
+ var requiredMaxCost = rec.requirements().maxCost();
+
+ return cost <= requiredMaxCost;
}
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
index fc78234..3cf22b7 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
@@ -1,12 +1,11 @@
package io.github.js0ny.ilp_coursework.service;
-import java.util.List;
-
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
+import java.util.List;
import org.springframework.stereotype.Service;
/**
@@ -49,6 +48,11 @@ public class GpsCalculationService {
return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);
}
+ public double calculateSteps(LngLat position1, LngLat position2) {
+ double distance = calculateDistance(position1, position2);
+ return distance / STEP;
+ }
+
/**
* Check if {@code position1} and
* {@code position2} are close to each other, the threshold is < 0.00015
@@ -99,8 +103,10 @@ public class GpsCalculationService {
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
* @see Region#isClosed()
*/
- public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException {
- if (!region.isClosed()) { // call method from RegionDto to check if not closed
+ public boolean checkIsInRegion(LngLat position, Region region)
+ throws IllegalArgumentException {
+ if (!region.isClosed()) {
+ // call method from RegionDto to check if not closed
throw new IllegalArgumentException("Region is not closed.");
}
return rayCasting(position, region.vertices());
@@ -146,7 +152,10 @@ public class GpsCalculationService {
continue;
}
- double xIntersection = a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());
+ double xIntersection =
+ a.lng() +
+ ((point.lat() - a.lat()) * (b.lng() - a.lng())) /
+ (b.lat() - a.lat());
if (xIntersection > point.lng()) {
++intersections;
@@ -171,14 +180,19 @@ public class GpsCalculationService {
*/
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat b) {
// Cross product: (p - a) × (b - a)
- double crossProduct = (p.lng() - a.lng()) * (b.lat() - a.lat())
- - (p.lat() - a.lat()) * (b.lng() - a.lng());
+ double crossProduct =
+ (p.lng() - a.lng()) * (b.lat() - a.lat()) -
+ (p.lat() - a.lat()) * (b.lng() - a.lng());
if (Math.abs(crossProduct) > 1e-9) {
return false;
}
- boolean isWithinLng = p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());
- boolean isWithinLat = p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());
+ boolean isWithinLng =
+ p.lng() >= Math.min(a.lng(), b.lng()) &&
+ p.lng() <= Math.max(a.lng(), b.lng());
+ boolean isWithinLat =
+ p.lat() >= Math.min(a.lat(), b.lat()) &&
+ p.lat() <= Math.max(a.lat(), b.lat());
return isWithinLng && isWithinLat;
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java
new file mode 100644
index 0000000..9f84f5f
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java
@@ -0,0 +1,143 @@
+package io.github.js0ny.ilp_coursework.service;
+
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import io.github.js0ny.ilp_coursework.data.common.Region;
+import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
+import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
+import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+
+@Service
+public class PathFinderService {
+
+ private final GpsCalculationService service;
+
+ public PathFinderService(GpsCalculationService gpsCalculationService) {
+ this.service = gpsCalculationService;
+ }
+
+
+ private static class Node implements Comparable {
+
+ final LngLat point;
+ Node parent;
+
+ double g;
+ double h;
+ double f;
+
+ public Node(LngLat point, Node parent, double g, double h) {
+ this.point = point;
+ this.parent = parent;
+ this.g = g;
+ this.h = h;
+ this.f = g + h;
+ }
+
+ @Override
+ public int compareTo(Node other) {
+ return Double.compare(this.f, other.f);
+ }
+ }
+
+
+ public static List findPath(
+ LngLat start,
+ LngLat target,
+ List restricted
+ ) {
+ var service = new GpsCalculationService();
+ PriorityQueue openSet = new PriorityQueue<>();
+ Map allNodesMinG = new HashMap<>();
+
+ if (checkIsInRestrictedAreas(target, restricted)) {
+ return Collections.emptyList();
+ }
+
+ Node startNode = new Node(
+ start,
+ null,
+ 0,
+ service.calculateDistance(start, target)
+ );
+ openSet.add(startNode);
+ allNodesMinG.put(start, 0.0);
+
+ while (!openSet.isEmpty()) {
+ Node current = openSet.poll();
+
+ if (service.isCloseTo(current.point, target)) {
+ return reconstructPath(current);
+ }
+
+ if (
+ current.g >
+ allNodesMinG.getOrDefault(current.point, Double.MAX_VALUE)
+ ) {
+ continue;
+ }
+
+ for (LngLat neighbour : getNeighbours(current.point)) {
+ if (checkIsInRestrictedAreas(neighbour, restricted)) {
+ continue;
+ }
+
+ double newG = current.g + 0.00015;
+
+ if (newG < allNodesMinG.getOrDefault(neighbour, Double.MAX_VALUE)) {
+ double newH = service.calculateDistance(neighbour, target);
+ Node neighbourNode = new Node(neighbour, current, newG, newH);
+ allNodesMinG.put(neighbour, newG);
+ openSet.add(neighbourNode);
+ }
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private static List reconstructPath(Node endNode) {
+ LinkedList path = new LinkedList<>();
+ Node curr = endNode;
+ while (curr != null) {
+ path.addFirst(curr.point);
+ curr = curr.parent;
+ }
+ return path;
+ }
+
+ private static boolean checkIsInRestrictedAreas(
+ LngLat point,
+ List RestrictedAreas
+ ) {
+ var service = new GpsCalculationService();
+ for (RestrictedArea area : RestrictedAreas) {
+ Region r = area.toRegion();
+ if (service.checkIsInRegion(point, r)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static List getNeighbours(LngLat p) {
+ var service = new GpsCalculationService();
+ double angle = 0;
+ List positions = new ArrayList<>();
+ final int directionCount = 8;
+ for (int i = 0; i < directionCount; i++) {
+ double directionAngle = angle + (i * 45);
+ LngLat nextPosition = service.nextPosition(p, directionAngle);
+ positions.add(nextPosition);
+ }
+ return positions;
+ }
+}
diff --git a/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java b/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
index d79f7aa..d99dc0d 100644
--- a/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
+++ b/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
@@ -14,9 +14,7 @@ import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
-
import java.util.List;
-
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -61,22 +59,19 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /distanceTo -> 200 OK")
void getDistance_shouldReturn200AndDistance_whenCorrectInput()
- throws Exception {
+ throws Exception {
double expected = 5.0;
String endpoint = "/api/v1/distanceTo";
LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2);
when(
- service.calculateDistance(
- any(LngLat.class),
- any(LngLat.class)
- )
+ service.calculateDistance(any(LngLat.class), any(LngLat.class))
).thenReturn(expected);
var mock = mockMvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(req))
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@@ -88,23 +83,23 @@ public class ApiControllerTest {
void getDistance_shouldReturn400_whenMissingField() throws Exception {
String endpoint = "/api/v1/distanceTo";
String req = """
- {
- "position1": {
- "lng": 3.0,
- "lat": 4.0
- }
+ {
+ "position1": {
+ "lng": 3.0,
+ "lat": 4.0
}
- """;
+ }
+ """;
when(
- service.calculateDistance(any(LngLat.class), isNull())
+ service.calculateDistance(any(LngLat.class), isNull())
).thenThrow(new NullPointerException());
mockMvc
- .perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(req)
- )
- .andExpect(status().isBadRequest());
+ .perform(
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(req)
+ )
+ .andExpect(status().isBadRequest());
}
}
@@ -115,19 +110,19 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isCloseTo -> 200 OK")
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput()
- throws Exception {
+ throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isCloseTo";
LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2);
when(
- service.isCloseTo(any(LngLat.class), any(LngLat.class))
+ service.isCloseTo(any(LngLat.class), any(LngLat.class))
).thenReturn(expected);
var mock = mockMvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(req))
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@@ -137,19 +132,19 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed()
- throws Exception {
+ throws Exception {
// json without a bracket
String malformedJson = """
- {
- "position1": { "lng": 0.0, "lat": 3.0 }
- """;
+ {
+ "position1": { "lng": 0.0, "lat": 3.0 }
+ """;
mockMvc
- .perform(
- post("/api/v1/isCloseTo")
- .contentType(MediaType.APPLICATION_JSON)
- .content(malformedJson)
- )
- .andExpect(status().isBadRequest());
+ .perform(
+ post("/api/v1/isCloseTo")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(malformedJson)
+ )
+ .andExpect(status().isBadRequest());
}
}
@@ -162,46 +157,46 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /nextPosition -> 200 OK")
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput()
- throws Exception {
+ throws Exception {
LngLat expected = new LngLat(0.00015, 0.0);
LngLat p = new LngLat(0, 0);
var req = new MovementRequest(p, 0);
when(
- service.nextPosition(any(LngLat.class), anyDouble())
+ service.nextPosition(any(LngLat.class), anyDouble())
).thenReturn(expected);
var mock = mockMvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(req))
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
mock.andExpect(
- content().json(objectMapper.writeValueAsString(expected))
+ content().json(objectMapper.writeValueAsString(expected))
);
}
@Test
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
void getNextPosition_shouldReturn400_whenKeyNameError()
- throws Exception {
+ throws Exception {
// "position" should be "start"
String malformedJson = """
- {
- "position": { "lng": 0.0, "lat": 3.0 },
- "angle": 180
- }
- """;
+ {
+ "position": { "lng": 0.0, "lat": 3.0 },
+ "angle": 180
+ }
+ """;
when(service.nextPosition(isNull(), anyDouble())).thenThrow(
- new NullPointerException()
+ new NullPointerException()
);
mockMvc
- .perform(
- post("/api/v1/nextPosition")
- .contentType(MediaType.APPLICATION_JSON)
- .content(malformedJson)
- )
- .andExpect(MockMvcResultMatchers.status().isBadRequest());
+ .perform(
+ post("/api/v1/nextPosition")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(malformedJson)
+ )
+ .andExpect(MockMvcResultMatchers.status().isBadRequest());
}
}
@@ -212,31 +207,28 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isInRegion -> 200 OK")
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput()
- throws Exception {
+ throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isInRegion";
var position = new LngLat(1.234, 1.222);
var region = new Region(
- "central",
- List.of(
- new LngLat(-3.192473, 55.946233),
- new LngLat(-3.192473, 55.942617),
- new LngLat(-3.184319, 55.942617),
- new LngLat(-3.184319, 55.946233),
- new LngLat(-3.192473, 55.946233)
- )
+ "central",
+ List.of(
+ new LngLat(-3.192473, 55.946233),
+ new LngLat(-3.192473, 55.942617),
+ new LngLat(-3.184319, 55.942617),
+ new LngLat(-3.184319, 55.946233),
+ new LngLat(-3.192473, 55.946233)
+ )
);
var req = new RegionCheckRequest(position, region);
when(
- service.checkIsInRegion(
- any(LngLat.class),
- any(Region.class)
- )
+ service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenReturn(expected);
var mock = mockMvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(req))
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@@ -245,59 +237,53 @@ public class ApiControllerTest {
@Test
@DisplayName(
- "POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
+ "POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
)
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments()
- throws Exception {
+ throws Exception {
var position = new LngLat(1, 1);
var region = new Region("illegal", List.of());
var request = new RegionCheckRequest(position, region);
when(
- service.checkIsInRegion(
- any(LngLat.class),
- any(Region.class)
- )
+ service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
- .perform(
- post("/api/v1/isInRegion")
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(request))
- )
- .andExpect(status().isBadRequest());
+ .perform(
+ post("/api/v1/isInRegion")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request))
+ )
+ .andExpect(status().isBadRequest());
}
@Test
@DisplayName(
- "POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
+ "POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
)
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices()
- throws Exception {
+ throws Exception {
var position = new LngLat(1, 1);
var region = new Region(
- "illegal",
- List.of(
- new LngLat(1, 2),
- new LngLat(3, 4),
- new LngLat(5, 6),
- new LngLat(7, 8),
- new LngLat(9, 10)
- )
+ "illegal",
+ List.of(
+ new LngLat(1, 2),
+ new LngLat(3, 4),
+ new LngLat(5, 6),
+ new LngLat(7, 8),
+ new LngLat(9, 10)
+ )
);
var request = new RegionCheckRequest(position, region);
when(
- service.checkIsInRegion(
- any(LngLat.class),
- any(Region.class)
- )
+ service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
- .perform(
- post("/api/v1/isInRegion")
- .contentType(MediaType.APPLICATION_JSON)
- .content(objectMapper.writeValueAsString(request))
- )
- .andExpect(status().isBadRequest());
+ .perform(
+ post("/api/v1/isInRegion")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request))
+ )
+ .andExpect(status().isBadRequest());
}
}
}
diff --git a/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java b/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
index 3eff87e..8edd77f 100644
--- a/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
+++ b/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
@@ -1,18 +1,17 @@
package io.github.js0ny.ilp_coursework.service;
-import io.github.js0ny.ilp_coursework.data.common.LngLat;
-import io.github.js0ny.ilp_coursework.data.common.Region;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.within;
+import io.github.js0ny.ilp_coursework.data.common.LngLat;
+import io.github.js0ny.ilp_coursework.data.common.Region;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
public class GpsCalculationServiceTest {
private static final double STEP = 0.00015;
@@ -29,6 +28,7 @@ public class GpsCalculationServiceTest {
@Nested
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
class CalculateDistanceTests {
+
@Test
@DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() {
@@ -92,6 +92,7 @@ public class GpsCalculationServiceTest {
@Nested
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
class IsCloseToTests {
+
@Test
@DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() {
@@ -112,7 +113,9 @@ public class GpsCalculationServiceTest {
}
@Test
- @DisplayName("True: Two points are close to each other and near threshold")
+ @DisplayName(
+ "True: Two points are close to each other and near threshold"
+ )
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00014);
@@ -156,12 +159,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
- @DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
+ @DisplayName(
+ "Cardinal Direction: nextPosition in North direction (90 degrees)"
+ )
void nextPosition_shouldMoveNorth_forAngle90() {
var start = new LngLat(0.0, 0.0);
double angle = 90;
@@ -170,12 +181,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
- @DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
+ @DisplayName(
+ "Cardinal Direction: nextPosition in West direction (180 degrees)"
+ )
void nextPosition_shouldMoveWest_forAngle180() {
var start = new LngLat(0.0, 0.0);
double angle = 180;
@@ -185,12 +204,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
- @DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
+ @DisplayName(
+ "Cardinal Direction: nextPosition in South direction (270 degrees)"
+ )
void nextPosition_shouldMoveSouth_forAngle270() {
var start = new LngLat(0.0, 0.0);
double angle = 270;
@@ -200,12 +227,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
- @DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
+ @DisplayName(
+ "Intercardinal Direction: nextPosition in Northeast direction (45 degrees)"
+ )
void nextPosition_shouldMoveNortheast_forAngle45() {
var start = new LngLat(0.0, 0.0);
double angle = 45;
@@ -216,8 +251,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
@@ -227,14 +268,22 @@ public class GpsCalculationServiceTest {
// 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
double angle = 405;
double equivalentAngle = 45;
- double expectedLng = STEP * Math.cos(Math.toRadians(equivalentAngle));
- double expectedLat = STEP * Math.sin(Math.toRadians(equivalentAngle));
+ double expectedLng =
+ STEP * Math.cos(Math.toRadians(equivalentAngle));
+ double expectedLat =
+ STEP * Math.sin(Math.toRadians(equivalentAngle));
var expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
@Test
@@ -249,8 +298,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
- assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
- assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
+ assertThat(actual.lng()).isCloseTo(
+ expected.lng(),
+ within(PRECISION)
+ );
+ assertThat(actual.lat()).isCloseTo(
+ expected.lat(),
+ within(PRECISION)
+ );
}
}
@@ -258,17 +313,31 @@ public class GpsCalculationServiceTest {
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
class CheckIsInRegionTests {
- public static final Region RECTANGLE_REGION = new Region("rectangle", List.of(new LngLat(0.0, 0.0),
- new LngLat(2.0, 0.0), new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, 0.0)));
+ public static final Region RECTANGLE_REGION = new Region(
+ "rectangle",
+ List.of(
+ new LngLat(0.0, 0.0),
+ new LngLat(2.0, 0.0),
+ new LngLat(2.0, 2.0),
+ new LngLat(0.0, 2.0),
+ new LngLat(0.0, 0.0)
+ )
+ );
@Test
@DisplayName("General Case: Given Example for Testing")
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
var position = new LngLat(1.234, 1.222);
- var region = new Region("central",
- List.of(new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.942617),
- new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.946233),
- new LngLat(-3.192473, 55.946233)));
+ var region = new Region(
+ "central",
+ List.of(
+ new LngLat(-3.192473, 55.946233),
+ new LngLat(-3.192473, 55.942617),
+ new LngLat(-3.184319, 55.942617),
+ new LngLat(-3.184319, 55.946233),
+ new LngLat(-3.192473, 55.946233)
+ )
+ );
boolean expected = false;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@@ -279,7 +348,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
var position = new LngLat(1.0, 1.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -288,7 +360,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
var position = new LngLat(3.0, 1.0);
boolean expected = false;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -296,10 +371,18 @@ public class GpsCalculationServiceTest {
@DisplayName("General Case: Simple Hexagon")
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
var position = new LngLat(2.0, 2.0);
- var region = new Region("hexagon",
- List.of(new LngLat(1.0, 0.0), new LngLat(4.0, 0.0), new LngLat(5.0, 2.0),
- new LngLat(4.0, 4.0), new LngLat(1.0, 4.0), new LngLat(0.0, 2.0),
- new LngLat(1.0, 0.0)));
+ var region = new Region(
+ "hexagon",
+ List.of(
+ new LngLat(1.0, 0.0),
+ new LngLat(4.0, 0.0),
+ new LngLat(5.0, 2.0),
+ new LngLat(4.0, 4.0),
+ new LngLat(1.0, 4.0),
+ new LngLat(0.0, 2.0),
+ new LngLat(1.0, 0.0)
+ )
+ );
boolean expected = true;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@@ -309,8 +392,15 @@ public class GpsCalculationServiceTest {
@DisplayName("Edge Case: Small Triangle")
void isInRegion_shouldReturnTrue_forSmallTriangle() {
var position = new LngLat(0.00001, 0.00001);
- var region = new Region("triangle", List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0),
- new LngLat(0.00005, 0.0001), new LngLat(0.0, 0.0)));
+ var region = new Region(
+ "triangle",
+ List.of(
+ new LngLat(0.0, 0.0),
+ new LngLat(0.0001, 0.0),
+ new LngLat(0.00005, 0.0001),
+ new LngLat(0.0, 0.0)
+ )
+ );
boolean expected = true;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@@ -321,7 +411,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
var position = new LngLat(0.0, 1.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -330,7 +423,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
var position = new LngLat(2.0, 1.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -339,7 +435,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
var position = new LngLat(0.0, 1.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -348,7 +447,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
var position = new LngLat(0.0, 0.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -357,7 +459,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
var position = new LngLat(2.0, 2.0);
boolean expected = true;
- boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
+ boolean actual = service.checkIsInRegion(
+ position,
+ RECTANGLE_REGION
+ );
assertThat(actual).isEqualTo(expected);
}
@@ -365,22 +470,40 @@ public class GpsCalculationServiceTest {
@DisplayName("Edge Case: Region not forming polygon")
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
var position = new LngLat(2.0, 2.0);
- var region = new Region("line",
- List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0), new LngLat(0.0, 0.0)));
+ var region = new Region(
+ "line",
+ List.of(
+ new LngLat(0.0, 0.0),
+ new LngLat(0.0001, 0.0),
+ new LngLat(0.0, 0.0)
+ )
+ );
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
- }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Region is not closed.");
}
@Test
@DisplayName("Edge Case: Region is not closed")
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
var position = new LngLat(2.0, 2.0);
- var region = new Region("rectangle", List.of(new LngLat(0.0, 0.0), new LngLat(2.0, 0.0),
- new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, -1.0)));
+ var region = new Region(
+ "rectangle",
+ List.of(
+ new LngLat(0.0, 0.0),
+ new LngLat(2.0, 0.0),
+ new LngLat(2.0, 2.0),
+ new LngLat(0.0, 2.0),
+ new LngLat(0.0, -1.0)
+ )
+ );
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
- }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Region is not closed.");
}
@Test
@@ -390,7 +513,9 @@ public class GpsCalculationServiceTest {
var region = new Region("rectangle", List.of());
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
- }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
+ })
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Region is not closed.");
}
}
}