From 141a957a8d9d84a22db87b4ccd1dadd571589102 Mon Sep 17 00:00:00 2001 From: js0ny Date: Mon, 24 Nov 2025 08:50:46 +0000 Subject: [PATCH] refractor(controller): Use List<> instead of [] --- .gitignore | 3 +- Dockerfile | 4 +- .../[POST] queryAvailableDrones/Complex.bru | 8 + .../[POST] queryAvailableDrones/Example.bru | 4 + .../Treat Null as False (Cooling).bru | 3 +- .../controller/ApiController.java | 14 +- .../controller/DroneController.java | 65 ++- .../data/common/AltitudeRange.java | 14 + .../data/common/DronePathDto.java | 4 - .../ilp_coursework/data/common/LngLat.java | 3 + .../ilp_coursework/data/common/LngLatAlt.java | 12 + .../data/common/MedRequirement.java | 9 - .../ilp_coursework/data/common/Region.java | 4 +- .../data/external/RestrictedArea.java | 23 + .../data/external/ServicePoint.java | 5 + .../data/external/ServicePointDrones.java | 2 +- .../data/request/MedDispatchRecRequest.java | 11 +- .../data/response/DeliveryPathResponse.java | 13 +- .../service/DroneAttrComparatorService.java | 140 ++++++ .../service/DroneInfoService.java | 422 +++++++++++------- .../service/GpsCalculationService.java | 32 +- .../service/PathFinderService.java | 143 ++++++ .../controller/ApiControllerTest.java | 196 ++++---- .../service/GpsCalculationServiceTest.java | 237 +++++++--- 24 files changed, 991 insertions(+), 380 deletions(-) create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/common/AltitudeRange.java delete mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/common/DronePathDto.java create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLatAlt.java delete mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/common/MedRequirement.java create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java create mode 100644 src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java 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."); } } }