diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Complex copy copy.bru b/ilp-cw-api/[POST] queryAvailableDrones/Complex copy copy.bru new file mode 100644 index 0000000..ee46799 --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/Complex copy copy.bru @@ -0,0 +1,39 @@ +meta { + name: Complex copy copy + type: http + seq: 5 +} + +post { + url: {{API_BASE}}/calcDeliveryPath + body: json + auth: inherit +} + +body:json { + [ + { + "id": 123, + "date": "2025-12-22", + "time": "14:30", + "requirements": { + "capacity": 0.75, + "heating": true, + "maxCost": 13.5 + }, + "delivery": { + "lng": -3.17, + "lat": 55.9 + } + } + ] +} + +assert { + res.status: eq 200 +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Complex copy.bru b/ilp-cw-api/[POST] queryAvailableDrones/Complex copy.bru new file mode 100644 index 0000000..77eba58 --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/Complex copy.bru @@ -0,0 +1,39 @@ +meta { + name: Complex copy + type: http + seq: 4 +} + +post { + url: {{API_BASE}}/calcDeliveryPathAsGeoJson + body: json + auth: inherit +} + +body:json { + [ + { + "id": 123, + "date": "2025-12-22", + "time": "14:30", + "requirements": { + "capacity": 0.75, + "heating": true, + "maxCost": 13.5 + }, + "delivery": { + "lng": -3.17, + "lat": 55.9 + } + } + ] +} + +assert { + res.status: eq 200 +} + +settings { + encodeUrl: true + timeout: 0 +} 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 1fba40b..dbd2147 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 @@ -7,6 +7,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 org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -15,10 +16,10 @@ import org.springframework.web.bind.annotation.RestController; /** * Main REST Controller for the ILP Coursework 1 application. - *

- * This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in CW1) - * This is responsible for mapping requests to the appropriate service method and returning the results as responses. - * The business logic is delegated to {@link GpsCalculationService} + * + *

This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in + * CW1) This is responsible for mapping requests to the appropriate service method and returning the + * results as responses. The business logic is delegated to {@link GpsCalculationService} */ @RestController @RequestMapping("/api/v1") @@ -27,9 +28,11 @@ public class ApiController { private final GpsCalculationService gpsService; /** - * Constructor of the {@code ApiController} with the business logic dependency {@code GpsCalculationService} + * Constructor of the {@code ApiController} with the business logic dependency {@code + * GpsCalculationService} * - * @param gpsService The service component that contains all business logic, injected by Spring's DI. + * @param gpsService The service component that contains all business logic, injected by + * Spring's DI. */ public ApiController(GpsCalculationService gpsService) { this.gpsService = gpsService; @@ -62,7 +65,8 @@ public class ApiController { * Handles POST requests to check if the two coordinates are close to each other * * @param request A {@link DistanceRequest} containing the two coordinates - * @return {@code true} if the distance is less than the predefined threshold, {@code false} otherwise + * @return {@code true} if the distance is less than the predefined threshold, {@code false} + * otherwise */ @PostMapping("/isCloseTo") public boolean getIsCloseTo(@RequestBody DistanceRequest request) { @@ -74,7 +78,8 @@ public class ApiController { /** * Handles POST requests to get the next position after an angle of movement * - * @param request A {@link MovementRequest} containing the start coordinate and angle of the movement. + * @param request A {@link MovementRequest} containing the start coordinate and angle of the + * movement. * @return A {@link LngLat} representing the destination */ @PostMapping("/nextPosition") 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 541a92d..712fa8d 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 @@ -7,17 +7,17 @@ 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 io.github.js0ny.ilp_coursework.service.PathFinderService; -import java.util.List; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; + +import java.util.List; /** * Main Rest Controller for the ILP Coursework 2 application. - *

- * This class handles incoming HTTP requests for the API under {@code /api/v1} - * path (defined in CW2) - * The business logic is delegated to {@link DroneInfoService} + * + *

This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in + * CW2) The business logic is delegated to {@link DroneInfoService} */ @RestController @RequestMapping("/api/v1") @@ -28,37 +28,33 @@ public class DroneController { private final PathFinderService pathFinderService; /** - * Constructor of the {@code DroneController} with the business logic dependency - * {@code DroneInfoService} - *

- * We handle the {@code baseUrl} here. Use a predefined URL if the environment - * variable {@code ILP_ENDPOINT} - * is not given. + * Constructor of the {@code DroneController} with the business logic dependency {@code + * DroneInfoService} + * + *

We handle the {@code baseUrl} here. Use a predefined URL if the environment variable + * {@code ILP_ENDPOINT} is not given. * * @param droneService The service component that contains all business logic */ public DroneController( - DroneInfoService droneService, - DroneAttrComparatorService droneAttrComparatorService, - PathFinderService pathFinderService - ) { + DroneInfoService droneService, + DroneAttrComparatorService droneAttrComparatorService, + PathFinderService pathFinderService) { this.droneInfoService = droneService; this.droneAttrComparatorService = droneAttrComparatorService; this.pathFinderService = pathFinderService; } /** - * Handles GET requests to retrieve an array of drones (identified by id) that - * has the capability of cooling + * Handles GET requests to retrieve an array of drones (identified by id) that has the + * capability of cooling * - * @param state The path variable that indicates the return should have or not - * have the capability + * @param state The path variable that indicates the return should have or not have the + * capability * @return An array of drone id with cooling capability. */ @GetMapping("/dronesWithCooling/{state}") - public List getDronesWithCoolingCapability( - @PathVariable boolean state - ) { + public List getDronesWithCoolingCapability(@PathVariable boolean state) { return droneInfoService.dronesWithCooling(state); } @@ -66,8 +62,8 @@ public class DroneController { * Handles GET requests to retrieve the drone detail identified by id * * @param id The id of the drone to be queried. - * @return 200 with {@link Drone}-style json if success, 404 if {@code id} - * not found, 400 otherwise + * @return 200 with {@link Drone}-style json if success, 404 if {@code id} not found, 400 + * otherwise */ @GetMapping("/droneDetails/{id}") public ResponseEntity getDroneDetail(@PathVariable String id) { @@ -80,51 +76,36 @@ public class DroneController { } /** - * Handles GET requests to retrieve an array of drone ids that - * {@code capability.attrName = attrVal} + * Handles GET requests to retrieve an array of drone ids that {@code capability.attrName = + * attrVal} * * @param attrName The name of the attribute to be queried - * @param attrVal The value of the attribute to be queried + * @param attrVal The value of the attribute to be queried * @return An array of drone id that matches the attribute name and value */ @GetMapping("/queryAsPath/{attrName}/{attrVal}") public List getIdByAttrMap( - @PathVariable String attrName, - @PathVariable String attrVal - ) { - return droneAttrComparatorService.dronesWithAttribute( - attrName, - attrVal - ); + @PathVariable String attrName, @PathVariable String attrVal) { + return droneAttrComparatorService.dronesWithAttribute(attrName, attrVal); } @PostMapping("/query") - public List getIdByAttrMapPost( - @RequestBody AttrQueryRequest[] attrComparators - ) { - return droneAttrComparatorService.dronesSatisfyingAttributes( - attrComparators - ); + public List getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) { + return droneAttrComparatorService.dronesSatisfyingAttributes(attrComparators); } @PostMapping("/queryAvailableDrones") - public List queryAvailableDrones( - @RequestBody MedDispatchRecRequest[] records - ) { + public List queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) { return droneInfoService.dronesMatchesRequirements(records); } @PostMapping("/calcDeliveryPath") - public DeliveryPathResponse calculateDeliveryPath( - @RequestBody MedDispatchRecRequest[] record - ) { + public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) { return pathFinderService.calculateDeliveryPath(record); } @PostMapping("/calcDeliveryPathAsGeoJson") - public String calculateDeliveryPathAsGeoJson( - @RequestBody MedDispatchRecRequest[] record - ) { + public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) { return pathFinderService.calculateDeliveryPathAsGeoJson(record); } } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java b/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java index a372ccb..4002fd5 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java @@ -1,14 +1,13 @@ package io.github.js0ny.ilp_coursework.controller; import com.fasterxml.jackson.core.JsonProcessingException; + import io.github.js0ny.ilp_coursework.service.DroneInfoService; -import org.springframework.stereotype.Controller; + import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/api/v1") public class GeoJsonDataController { 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 index 519a371..b4ca9b2 100644 --- 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 @@ -6,9 +6,7 @@ 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. - * + * @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) { -} +public record AltitudeRange(double lower, double upper) {} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java index bcdd0c2..56c87bd 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java @@ -11,8 +11,7 @@ public record Angle(double degrees) { public Angle { if (degrees < 0 || degrees >= 360) { - throw new IllegalArgumentException( - "Angle must be in range [0, 360). Got: " + degrees); + throw new IllegalArgumentException("Angle must be in range [0, 360). Got: " + degrees); } // Should be a multiple of 22.5 (one of the 16 major directions) @@ -22,18 +21,16 @@ public record Angle(double degrees) { // 1.0e-15 // So we need to check if the remainder is small enough, or close enough to STEP // (handling negative errors) - if (Math.abs(remainder) > EPSILON && - Math.abs(remainder - STEP) > EPSILON) { + if (Math.abs(remainder) > EPSILON && Math.abs(remainder - STEP) > EPSILON) { throw new IllegalArgumentException( - "Angle must be a multiple of 22.5 (one of the 16 major directions). Got: " + - degrees); + "Angle must be a multiple of 22.5 (one of the 16 major directions). Got: " + + degrees); } } public static Angle fromIndex(int index) { if (index < 0 || index > 15) { - throw new IllegalArgumentException( - "Direction index must be between 0 and 15"); + throw new IllegalArgumentException("Direction index must be between 0 and 15"); } return new Angle(index * STEP); } @@ -64,5 +61,4 @@ public record Angle(double degrees) { public double toRadians() { return Math.toRadians(degrees); } - } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneAvailability.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneAvailability.java index 231fdfd..d4c1c5b 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneAvailability.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneAvailability.java @@ -3,9 +3,7 @@ package io.github.js0ny.ilp_coursework.data.common; import java.time.DayOfWeek; import java.time.LocalTime; -public record DroneAvailability( - String id, - TimeWindow[] availability) { +public record DroneAvailability(String id, TimeWindow[] availability) { public boolean checkAvailability(DayOfWeek day, LocalTime time) { diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneCapability.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneCapability.java index 66689b1..0d9d016 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneCapability.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneCapability.java @@ -7,6 +7,4 @@ public record DroneCapability( int maxMoves, float costPerMove, float costInitial, - float costFinal) { - -} + float costFinal) {} 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 99ac2ea..9744299 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 @@ -1,8 +1,8 @@ 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 + * 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 @@ -13,14 +13,12 @@ public record LngLat(double lng, double lat) { public LngLat { if (lat < -90 || lat > 90) { throw new IllegalArgumentException( - "Latitude must be between -90 and +90 degrees. Got: " + lat - ); + "Latitude must be between -90 and +90 degrees. Got: " + lat); } if (lng < -180 || lng > 180) { throw new IllegalArgumentException( - "Longitude must be between -180 and +180 degrees. Got: " + lng - ); + "Longitude must be between -180 and +180 degrees. Got: " + lng); } } @@ -32,9 +30,6 @@ public record LngLat(double lng, double lat) { if (other == null) { return false; } - return ( - Math.abs(lng - other.lng()) < EPSILON && - Math.abs(lat - other.lat()) < EPSILON - ); + return (Math.abs(lng - other.lng()) < EPSILON && Math.abs(lat - other.lat()) < EPSILON); } } 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 index 6d4544c..a3a06de 100644 --- 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 @@ -1,12 +1,11 @@ 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 + * 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) { -} +public record LngLatAlt(double lng, double lat, double alt) {} 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 ff002a4..e9f95a2 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,47 +1,42 @@ package io.github.js0ny.ilp_coursework.data.common; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest; import java.util.List; -import java.util.Objects; - -import com.fasterxml.jackson.databind.ObjectMapper; - import java.util.Map; +import java.util.Objects; /** * Represents the data transfer object for a region definition - *

- * This record encapsulates the data for calculating if a coordinate is inside - * the region - *

- * A built-in method {@code isClosedTo} is defined to check this DTO is valid or - * not in the mean of closing polygon * - * @param name The human-readable name for the region + *

This record encapsulates the data for calculating if a coordinate is inside the region + * + *

A built-in method {@code isClosedTo} is defined to check this DTO is valid or not in the mean + * of closing polygon + * + * @param name The human-readable name for the region * @param vertices list of coordinates that forms a polygon as a region. - *

- * In order to define a valid region, the last element of the - * list should be the same as the first, or - * known as closed + *

In order to define a valid region, the last element of the list should be the same as the + * first, or known as closed * @see RegionCheckRequest - * @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat, - * Region) + * @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat, Region) */ public record Region(String name, List vertices) { /** * Magic number 4: For a polygon, 3 edges is required. - *

- * In this dto, edges + 1 vertices is required. + * + *

In this dto, edges + 1 vertices is required. */ private static final int MINIMUM_VERTICES = 4; /** - * Method to check if the region has a valid polygon by checking if the - * {@code vertices} forms a closed polygon + * Method to check if the region has a valid polygon by checking if the {@code vertices} forms a + * closed polygon * - * @return {@code true} if the {@code vertices} are able to form a polygon and - * form a closed polygon + * @return {@code true} if the {@code vertices} are able to form a polygon and form a closed + * polygon */ public boolean isClosed() { if (vertices == null || vertices.size() < MINIMUM_VERTICES) { @@ -56,13 +51,10 @@ public record Region(String name, List vertices) { try { ObjectMapper mapper = new ObjectMapper(); - List> ring = vertices.stream() - .map(v -> List.of(v.lng(), v.lat())) - .toList(); + List> ring = + vertices.stream().map(v -> List.of(v.lng(), v.lat())).toList(); - return Map.of( - "type", "Polygon", - "coordinates", List.of(ring)); + return Map.of("type", "Polygon", "coordinates", List.of(ring)); } catch (Exception e) { throw new RuntimeException("Failed to generate GeoJSON", e); diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/TimeWindow.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/TimeWindow.java index 04daeae..25bf152 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/TimeWindow.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/TimeWindow.java @@ -3,8 +3,4 @@ package io.github.js0ny.ilp_coursework.data.common; import java.time.DayOfWeek; import java.time.LocalTime; -public record TimeWindow( - DayOfWeek dayOfWeek, - LocalTime from, - LocalTime until) { -} +public record TimeWindow(DayOfWeek dayOfWeek, LocalTime from, LocalTime until) {} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/external/Drone.java b/src/main/java/io/github/js0ny/ilp_coursework/data/external/Drone.java index 29a6c2d..0965663 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/external/Drone.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/external/Drone.java @@ -2,13 +2,8 @@ package io.github.js0ny.ilp_coursework.data.external; import io.github.js0ny.ilp_coursework.data.common.DroneCapability; -/** - * Represents the data transfer object for a drone, gained from the endpoints - */ -public record Drone( - String name, - String id, - DroneCapability capability) { +/** Represents the data transfer object for a drone, gained from the endpoints */ +public record Drone(String name, String id, DroneCapability capability) { public int parseId() { try { 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 index c9e9e10..069c0c8 100644 --- 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 @@ -4,15 +4,11 @@ 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 record RestrictedArea(String name, int id, AltitudeRange limits, LngLatAlt[] vertices) { public Region toRegion() { List vertices2D = new ArrayList<>(); for (var vertex : vertices) { 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 8f5cbd8..d98c7f5 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 @@ -1,11 +1,10 @@ package io.github.js0ny.ilp_coursework.data.external; import io.github.js0ny.ilp_coursework.data.common.DroneAvailability; + import org.springframework.lang.Nullable; -public record ServicePointDrones( - int servicePointId, - DroneAvailability[] drones) { +public record ServicePointDrones(int servicePointId, DroneAvailability[] drones) { @Nullable public DroneAvailability locateDroneById(String droneId) { diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/request/AttrQueryRequest.java b/src/main/java/io/github/js0ny/ilp_coursework/data/request/AttrQueryRequest.java index f87a351..f3420b6 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/request/AttrQueryRequest.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/request/AttrQueryRequest.java @@ -3,5 +3,4 @@ package io.github.js0ny.ilp_coursework.data.request; // TODO: Convert operator to Enum // import io.github.js0ny.ilp_coursework.util.AttrOperator; -public record AttrQueryRequest(String attribute, String operator, String value) { -} +public record AttrQueryRequest(String attribute, String operator, String value) {} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/request/DistanceRequest.java b/src/main/java/io/github/js0ny/ilp_coursework/data/request/DistanceRequest.java index 34ac900..045ecf3 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/request/DistanceRequest.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/request/DistanceRequest.java @@ -4,12 +4,11 @@ import io.github.js0ny.ilp_coursework.data.common.LngLat; /** * Represents the data transfer object for a distance operation request. - *

- * This record encapsulates the data for several endpoints that involves two {@code LngLatDto} + * + *

This record encapsulates the data for several endpoints that involves two {@code LngLatDto} * and serves as the data contract for those API operation * * @param position1 Nested object of {@link LngLat} * @param position2 Nested object of {@link LngLat} */ -public record DistanceRequest(LngLat position1, LngLat position2) { -} +public record DistanceRequest(LngLat position1, LngLat position2) {} 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 57ee11f..307f213 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,6 +1,7 @@ package io.github.js0ny.ilp_coursework.data.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import io.github.js0ny.ilp_coursework.data.common.LngLat; import java.time.LocalDate; @@ -8,17 +9,7 @@ import java.time.LocalTime; @JsonIgnoreProperties(ignoreUnknown = true) public record MedDispatchRecRequest( - int id, - LocalDate date, - LocalTime time, - MedRequirement requirements, - LngLat delivery) { + int id, LocalDate date, LocalTime time, MedRequirement requirements, LngLat delivery) { @JsonIgnoreProperties(ignoreUnknown = true) - public record MedRequirement( - float capacity, - boolean cooling, - boolean heating, - float maxCost - ) { - } + public record MedRequirement(float capacity, boolean cooling, boolean heating, float maxCost) {} } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/request/MovementRequest.java b/src/main/java/io/github/js0ny/ilp_coursework/data/request/MovementRequest.java index 2eedade..fd364ac 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/request/MovementRequest.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/request/MovementRequest.java @@ -4,14 +4,13 @@ import io.github.js0ny.ilp_coursework.data.common.LngLat; /** * Represents the data transfer object for a movement action request. - *

- * This record encapsulates the data for endpoint /api/v1/nextPosition and serves as the data contract for - * this API operation + * + *

This record encapsulates the data for endpoint /api/v1/nextPosition and serves as the data + * contract for this API operation * * @param start The starting coordinate of the movement - * @param angle The angle to movement in degree. This corresponds to compass directions. - * For example: 0 for East, 90 for North + * @param angle The angle to movement in degree. This corresponds to compass directions. For + * example: 0 for East, 90 for North * @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest) */ -public record MovementRequest(LngLat start, double angle) { -} +public record MovementRequest(LngLat start, double angle) {} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/request/RegionCheckRequest.java b/src/main/java/io/github/js0ny/ilp_coursework/data/request/RegionCheckRequest.java index 959253c..382edcd 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/request/RegionCheckRequest.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/request/RegionCheckRequest.java @@ -5,15 +5,14 @@ import io.github.js0ny.ilp_coursework.data.common.Region; /** * Represents the data transfer object for a region check request. - *

- * This record encapsulates the data for endpoint /api/v1/isInRegion and serves as the data contract for - * this API operation + * + *

This record encapsulates the data for endpoint /api/v1/isInRegion and serves as the data + * contract for this API operation + * *

* * @param position The coordinate to be checked - * @param region The region for the check. - * This is a nested object represented by {@link Region} + * @param region The region for the check. This is a nested object represented by {@link Region} * @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest) */ -public record RegionCheckRequest(LngLat position, Region region) { -} +public record RegionCheckRequest(LngLat position, Region region) {} 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 e029594..78d0998 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,13 +1,10 @@ package io.github.js0ny.ilp_coursework.data.response; import io.github.js0ny.ilp_coursework.data.common.LngLat; + import java.util.List; -public record DeliveryPathResponse( - float totalCost, - int totalMoves, - DronePath[] dronePaths -) { +public record DeliveryPathResponse(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/exception/GlobalExceptionHandler.java b/src/main/java/io/github/js0ny/ilp_coursework/exception/GlobalExceptionHandler.java index 1134f1e..258e64c 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/exception/GlobalExceptionHandler.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/exception/GlobalExceptionHandler.java @@ -1,8 +1,5 @@ package io.github.js0ny.ilp_coursework.exception; -import java.util.Map; -import java.util.Optional; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -11,15 +8,17 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -/** - * Class that handles exception or failed request. Map all error requests to 400. - */ +import java.util.Map; +import java.util.Optional; + +/** Class that handles exception or failed request. Map all error requests to 400. */ @RestControllerAdvice public class GlobalExceptionHandler { /// Use a logger to save logs instead of passing them to user private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - private final Map badRequestMap = Map.of("status", "400", "error", "Bad Request"); + private final Map badRequestMap = + Map.of("status", "400", "error", "Bad Request"); @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -31,8 +30,8 @@ public class GlobalExceptionHandler { @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map handleIllegalArgument(IllegalArgumentException ex) { - String errorMessage = Optional.ofNullable(ex.getMessage()) - .orElse("Invalid argument provided."); + String errorMessage = + Optional.ofNullable(ex.getMessage()).orElse("Invalid argument provided."); log.warn("Illegal argument in request: {}", errorMessage); return badRequestMap; } 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 index 94f0061..e74c67f 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneAttrComparatorService.java @@ -4,9 +4,14 @@ 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 org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -14,8 +19,6 @@ 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 { @@ -24,14 +27,11 @@ public class DroneAttrComparatorService { private final String dronesEndpoint = "drones"; private final RestTemplate restTemplate = new RestTemplate(); - /** - * Constructor, handles the base url here. - */ + /** 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/"; + this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/"; } else { // Defensive: Add '/' to the end of the URL if (!baseUrl.endsWith("/")) { @@ -43,11 +43,11 @@ public class DroneAttrComparatorService { /** * Return an array of ids of drones with a given attribute name and value. - *

- * Associated service method with {@code /queryAsPath/{attrName}/{attrVal}} + * + *

Associated service method with {@code /queryAsPath/{attrName}/{attrVal}} * * @param attrName the attribute name to filter on - * @param attrVal the attribute value 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 @@ -58,26 +58,19 @@ public class DroneAttrComparatorService { } /** - * Return an array of ids of drones which matches all given complex comparing - * rules + * 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 - ) { + 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 - ); + List ids = dronesWithAttributeCompared(attribute, value, op); if (matchingDroneIds == null) { matchingDroneIds = new HashSet<>(ids); } else { @@ -92,25 +85,20 @@ public class DroneAttrComparatorService { /** * 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, + * + *

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) + * @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 - ) { + 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); @@ -124,17 +112,18 @@ public class DroneAttrComparatorService { 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()); + .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 29f101a..ca34ecc 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 @@ -2,6 +2,7 @@ package io.github.js0ny.ilp_coursework.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; + 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.Drone; @@ -9,30 +10,29 @@ 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.MedDispatchRecRequest; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + import java.net.URI; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalTime; import java.util.*; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; @Service public class DroneInfoService { private final String baseUrl; - 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; - /** - * Constructor, handles the base url here. - */ + /** Constructor, handles the base url here. */ public DroneInfoService() { this(new RestTemplate()); } @@ -41,8 +41,7 @@ public class DroneInfoService { this.restTemplate = restTemplate; 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("/")) { @@ -54,13 +53,14 @@ public class DroneInfoService { /** * Return an array of ids of drones with/without cooling capability - *

- * Associated service method with {@code /dronesWithCooling/{state}} + * + *

Associated service method with {@code /dronesWithCooling/{state}} * * @param state determines the capability filtering - * @return if {@code state} is true, return ids of drones with cooling - * capability, else without cooling - * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) + * @return if {@code state} is true, return ids of drones with cooling capability, else without + * cooling + * @see + * io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) */ public List dronesWithCooling(boolean state) { // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); @@ -71,25 +71,22 @@ public class DroneInfoService { return new ArrayList<>(); } - return drones - .stream() - .filter(drone -> drone.capability().cooling() == state) - .map(Drone::id) - .collect(Collectors.toList()); + return drones.stream() + .filter(drone -> drone.capability().cooling() == state) + .map(Drone::id) + .collect(Collectors.toList()); } /** * Return a {@link Drone}-style json data structure with the given {@code id} - *

- * Associated service method with {@code /droneDetails/{id}} + * + *

Associated service method with {@code /droneDetails/{id}} * * @param id The id of the drone * @return drone json body of given id - * @throws NullPointerException when cannot fetch available drones from - * remote - * @throws IllegalArgumentException when drone with given {@code id} cannot be - * found - * this should lead to a 404 + * @throws NullPointerException when cannot fetch available drones from remote + * @throws IllegalArgumentException when drone with given {@code id} cannot be found this should + * lead to a 404 * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String) */ public Drone droneDetail(String id) { @@ -102,16 +99,14 @@ public class DroneInfoService { } // This will result in 404 - throw new IllegalArgumentException( - "drone with that ID cannot be found" - ); + throw new IllegalArgumentException("drone with that ID cannot be found"); } /** - * Return an array of ids of drones that match all the requirements in the - * medical dispatch records - *

- * Associated service method with + * Return an array of ids of drones that match all the requirements in the medical dispatch + * records + * + *

Associated service method with * * @param rec array of medical dispatch records * @return List of drone ids that match all the requirements @@ -121,51 +116,43 @@ public class DroneInfoService { List drones = fetchAllDrones(); if (rec == null || rec.length == 0) { - return drones - .stream() - .filter(Objects::nonNull) - .map(Drone::id) - .collect(Collectors.toList()); + return drones.stream() + .filter(Objects::nonNull) + .map(Drone::id) + .collect(Collectors.toList()); } /* * Traverse and filter drones, pass every record's requirement to helper */ - return drones - .stream() - .filter(d -> d != null && d.capability() != null) - .filter(d -> - Arrays.stream(rec) - .filter(r -> r != null && r.requirements() != null) - .allMatch(r -> droneMatchesRequirement(d, r)) - ) - .map(Drone::id) - .collect(Collectors.toList()); + return drones.stream() + .filter(d -> d != null && d.capability() != null) + .filter( + d -> + Arrays.stream(rec) + .filter(r -> r != null && r.requirements() != null) + .allMatch(r -> droneMatchesRequirement(d, r))) + .map(Drone::id) + .collect(Collectors.toList()); } /** * Helper to check if a drone meets the requirement of a medical dispatch. * - * @param drone the drone to be checked + * @param drone the drone to be checked * @param record the medical dispatch record containing the requirement * @return true if the drone meets the requirement, false otherwise - * @throws IllegalArgumentException when record requirements or drone capability - * is invalid (capacity and id cannot be null - * in {@code MedDispathRecDto}) + * @throws IllegalArgumentException when record requirements or drone capability is invalid + * (capacity and id cannot be null in {@code MedDispathRecDto}) */ - public boolean droneMatchesRequirement( - Drone drone, - MedDispatchRecRequest record - ) { + public boolean droneMatchesRequirement(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(); @@ -187,11 +174,7 @@ public class DroneInfoService { // Conditions: All requirements matched + availability matched, use helper // For minimal privilege, only pass drone id to check availability - return ( - matchesCooling && - matchesHeating && - checkAvailability(drone.id(), record) - ); // && + return (matchesCooling && matchesHeating && checkAvailability(drone.id(), record)); // && // checkCost(drone, record) // checkCost is more expensive than // checkAvailability } @@ -200,21 +183,13 @@ public class DroneInfoService { * Helper to check if a drone is available at the required date and time * * @param droneId the id of the drone to be checked - * @param record the medical dispatch record containing the required date and - * time + * @param record the medical dispatch record containing the required date and time * @return true if the drone is available, false otherwise */ - private boolean checkAvailability( - String droneId, - MedDispatchRecRequest record - ) { - URI droneUrl = URI.create(baseUrl).resolve( - dronesForServicePointsEndpoint - ); - ServicePointDrones[] servicePoints = restTemplate.getForObject( - droneUrl, - ServicePointDrones[].class - ); + private boolean checkAvailability(String droneId, MedDispatchRecRequest record) { + URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); + ServicePointDrones[] servicePoints = + restTemplate.getForObject(droneUrl, ServicePointDrones[].class); LocalDate requiredDate = record.date(); DayOfWeek requiredDay = requiredDate.getDayOfWeek(); @@ -232,13 +207,9 @@ public class DroneInfoService { } private LngLat queryServicePointLocationByDroneId(String droneId) { - URI droneUrl = URI.create(baseUrl).resolve( - dronesForServicePointsEndpoint - ); - ServicePointDrones[] servicePoints = restTemplate.getForObject( - droneUrl, - ServicePointDrones[].class - ); + URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); + ServicePointDrones[] servicePoints = + restTemplate.getForObject(droneUrl, ServicePointDrones[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -253,14 +224,10 @@ public class DroneInfoService { @Nullable private LngLat queryServicePointLocation(int id) { - URI servicePointUrl = URI.create(baseUrl).resolve( - servicePointsEndpoint - ); + URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); - ServicePoint[] servicePoints = restTemplate.getForObject( - servicePointUrl, - ServicePoint[].class - ); + ServicePoint[] servicePoints = + restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -282,49 +249,32 @@ public class DroneInfoService { } public List fetchRestrictedAreas() { - URI restrictedUrl = URI.create(baseUrl).resolve( - restrictedAreasEndpoint - ); - RestrictedArea[] restrictedAreas = restTemplate.getForObject( - restrictedUrl, - RestrictedArea[].class - ); + URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint); + RestrictedArea[] restrictedAreas = + restTemplate.getForObject(restrictedUrl, RestrictedArea[].class); assert restrictedAreas != null; return Arrays.asList(restrictedAreas); } - public List fetchRestrictedAreasInGeoJson() - throws JsonProcessingException { + public List fetchRestrictedAreasInGeoJson() throws JsonProcessingException { var mapper = new ObjectMapper(); var ras = fetchRestrictedAreas(); - var geoJson = ras - .stream() - .map(RestrictedArea::toRegion) - .map(Region::toGeoJson) - .toList(); + var geoJson = ras.stream().map(RestrictedArea::toRegion).map(Region::toGeoJson).toList(); return Collections.singletonList(mapper.writeValueAsString(geoJson)); } public List fetchServicePoints() { - URI servicePointUrl = URI.create(baseUrl).resolve( - servicePointsEndpoint - ); - ServicePoint[] servicePoints = restTemplate.getForObject( - servicePointUrl, - ServicePoint[].class - ); + URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); + ServicePoint[] servicePoints = + restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; return Arrays.asList(servicePoints); } public List fetchDronesForServicePoints() { - URI servicePointDronesUrl = URI.create(baseUrl).resolve( - dronesForServicePointsEndpoint - ); - ServicePointDrones[] servicePointDrones = restTemplate.getForObject( - servicePointDronesUrl, - ServicePointDrones[].class - ); + URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); + ServicePointDrones[] servicePointDrones = + restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class); assert servicePointDrones != null; return Arrays.asList(servicePointDrones); } 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 2e3abd1..e1f9998 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 @@ -6,9 +6,11 @@ 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; +import java.util.List; + /** * Class that handles calculations about Coordinates * @@ -24,6 +26,7 @@ public class GpsCalculationService { * @see #nextPosition(LngLat, double) */ private static final double STEP = 0.00015; + /** * Given threshold to judge if two points are close to each other * @@ -32,14 +35,12 @@ public class GpsCalculationService { private static final double CLOSE_THRESHOLD = 0.00015; /** - * Calculate the Euclidean distance between {@code position1} and - * {@code position2}, which are coordinates - * defined as {@link LngLat} + * Calculate the Euclidean distance between {@code position1} and {@code position2}, which are + * coordinates defined as {@link LngLat} * * @param position1 The coordinate of the first position * @param position2 The coordinate of the second position - * @return The Euclidean distance between {@code position1} and - * {@code position2} + * @return The Euclidean distance between {@code position1} and {@code position2} * @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest) */ public double calculateDistance(LngLat position1, LngLat position2) { @@ -55,17 +56,14 @@ public class GpsCalculationService { } /** - * Check if {@code position1} and - * {@code position2} are close to each other, the threshold is < 0.00015 + * Check if {@code position1} and {@code position2} are close to each other, the threshold is < + * 0.00015 * - *

- * Note that = 0.00015 will be counted as not close to and will return {@code - * false} + *

Note that = 0.00015 will be counted as not close to and will return {@code false} * * @param position1 The coordinate of the first position * @param position2 The coordinate of the second position - * @return {@code true} if {@code position1} and - * {@code position2} are close to each other + * @return {@code true} if {@code position1} and {@code position2} are close to each other * @see #CLOSE_THRESHOLD * @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsCloseTo(DistanceRequest) */ @@ -75,9 +73,8 @@ public class GpsCalculationService { } /** - * Returns the next position moved from {@code start} in the direction with - * {@code angle}, with step size - * 0.00015 + * Returns the next position moved from {@code start} in the direction with {@code angle}, with + * step size 0.00015 * * @param start The coordinate of the original start point. * @param angle The direction to be moved in angle. @@ -93,19 +90,18 @@ public class GpsCalculationService { } /** - * Used to check if the given {@code position} - * is inside the {@code region}, on edge and vertex is considered as inside. + * Used to check if the given {@code position} is inside the {@code region}, on edge and vertex + * is considered as inside. * * @param position The coordinate of the position. - * @param region A {@link Region} that contains name and a list of - * {@code LngLatDto} + * @param region A {@link Region} that contains name and a list of {@code LngLatDto} * @return {@code true} if {@code position} is inside the {@code region}. * @throws IllegalArgumentException If {@code region} is not closed - * @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest) + * @see + * io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest) * @see Region#isClosed() */ - public boolean checkIsInRegion(LngLat position, Region region) - throws IllegalArgumentException { + 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."); @@ -114,14 +110,12 @@ public class GpsCalculationService { } /** - * Helper function to {@code checkIsInRegion}, use of ray-casting algorithm - * to check if inside the polygon + * Helper function to {@code checkIsInRegion}, use of ray-casting algorithm to check if inside + * the polygon * - * @param point The point to check - * @param polygon The region that forms a polygon to check if {@code point} - * sits inside. - * @return If the {@code point} sits inside the {@code polygon} then - * return {@code true} + * @param point The point to check + * @param polygon The region that forms a polygon to check if {@code point} sits inside. + * @return If the {@code point} sits inside the {@code polygon} then return {@code true} * @see #isPointOnEdge(LngLat, LngLat, LngLat) * @see #checkIsInRegion(LngLat, Region) */ @@ -154,9 +148,7 @@ public class GpsCalculationService { } double xIntersection = - a.lng() + - ((point.lat() - a.lat()) * (b.lng() - a.lng())) / - (b.lat() - a.lat()); + a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat()); if (xIntersection > point.lng()) { ++intersections; @@ -170,8 +162,7 @@ public class GpsCalculationService { /** * Helper function from {@code rayCasting} that used to simply calculation
- * Used to check if point {@code p} is on the edge formed by - * {@code a} and {@code b} + * Used to check if point {@code p} is on the edge formed by {@code a} and {@code b} * * @param p point to be checked on the edge * @param a point that forms the edge @@ -182,18 +173,16 @@ 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()); + (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()); + 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()); + 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 index 0d26328..6e0a4cf 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java @@ -19,6 +19,9 @@ 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; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery; + +import org.springframework.stereotype.Service; + import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -27,7 +30,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.springframework.stereotype.Service; /** * Class that handles calculations about deliverypath @@ -40,9 +42,8 @@ import org.springframework.stereotype.Service; public class PathFinderService { /** - * Hard stop on how many pathfinding iterations we attempt for a single - * segment before bailing, useful for preventing infinite loops caused by - * precision quirks or unexpected map data. + * Hard stop on how many pathfinding iterations we attempt for a single segment before bailing, + * useful for preventing infinite loops caused by precision quirks or unexpected map data. * * @see #computePath(LngLat, LngLat) */ @@ -59,17 +60,14 @@ public class PathFinderService { private final List restrictedRegions; /** - * Constructor for PathFinderService. The dependencies are injected by - * Spring and the constructor pre-computes reference maps used throughout the - * request lifecycle. + * Constructor for PathFinderService. The dependencies are injected by Spring and the + * constructor pre-computes reference maps used throughout the request lifecycle. * * @param gpsCalculationService Service handling geometric operations. - * @param droneInfoService Service that exposes drone metadata and - * capability information. + * @param droneInfoService Service that exposes drone metadata and capability information. */ public PathFinderService( - GpsCalculationService gpsCalculationService, - DroneInfoService droneInfoService) { + GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) { this.gpsCalculationService = gpsCalculationService; this.droneInfoService = droneInfoService; this.objectMapper = new ObjectMapper(); @@ -79,11 +77,11 @@ public class PathFinderService { this.drones = droneInfoService.fetchAllDrones(); List servicePoints = droneInfoService.fetchServicePoints(); - List servicePointAssignments = droneInfoService.fetchDronesForServicePoints(); + List servicePointAssignments = + droneInfoService.fetchDronesForServicePoints(); List restrictedAreas = droneInfoService.fetchRestrictedAreas(); - this.droneById = this.drones.stream().collect( - Collectors.toMap(Drone::id, drone -> drone)); + this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone)); this.droneServicePointMap = new HashMap<>(); for (ServicePointDrones assignment : servicePointAssignments) { @@ -94,40 +92,33 @@ public class PathFinderService { if (availability == null || availability.id() == null) { continue; } - droneServicePointMap.put( - availability.id(), - assignment.servicePointId()); + droneServicePointMap.put(availability.id(), assignment.servicePointId()); } } - this.servicePointLocations = servicePoints - .stream() - .collect( - Collectors.toMap(ServicePoint::id, sp -> new LngLat(sp.location()))); + this.servicePointLocations = + servicePoints.stream() + .collect( + Collectors.toMap( + ServicePoint::id, sp -> new LngLat(sp.location()))); - this.restrictedRegions = restrictedAreas - .stream() - .map(RestrictedArea::toRegion) - .toList(); + this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList(); } /** - * Produce a delivery plan for the provided dispatch records. Deliveries are - * grouped per compatible drone and per trip to satisfy each drone move - * limit. + * Produce a delivery plan for the provided dispatch records. Deliveries are grouped per + * compatible drone and per trip to satisfy each drone move limit. * * @param records Dispatch records to be fulfilled. * @return Aggregated path response with cost and move totals. * @see #calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[]) */ - public DeliveryPathResponse calculateDeliveryPath( - MedDispatchRecRequest[] records) { + public DeliveryPathResponse calculateDeliveryPath(MedDispatchRecRequest[] records) { if (records == null || records.length == 0) { return new DeliveryPathResponse(0f, 0, new DronePath[0]); } - Map> assigned = assignDeliveries( - records); + Map> assigned = assignDeliveries(records); List paths = new ArrayList<>(); float totalCost = 0f; @@ -148,25 +139,20 @@ public class PathFinderService { continue; } - List sortedDeliveries = entry - .getValue() - .stream() - .sorted( - Comparator.comparingDouble(rec -> gpsCalculationService.calculateDistance( - servicePointLocation, - rec.delivery()))) - .toList(); + List sortedDeliveries = + entry.getValue().stream() + .sorted( + Comparator.comparingDouble( + rec -> + gpsCalculationService.calculateDistance( + servicePointLocation, rec.delivery()))) + .toList(); - List> trips = splitTrips( - sortedDeliveries, - drone, - servicePointLocation); + List> trips = + splitTrips(sortedDeliveries, drone, servicePointLocation); for (List trip : trips) { - TripResult result = buildTrip( - drone, - servicePointLocation, - trip); + TripResult result = buildTrip(drone, servicePointLocation, trip); if (result != null) { totalCost += result.cost(); totalMoves += result.moves(); @@ -175,23 +161,18 @@ public class PathFinderService { } } - return new DeliveryPathResponse( - totalCost, - totalMoves, - paths.toArray(new DronePath[0])); + return new DeliveryPathResponse(totalCost, totalMoves, paths.toArray(new DronePath[0])); } /** - * Convenience wrapper around {@link #calculateDeliveryPath} that serializes - * the result into a GeoJSON FeatureCollection suitable for mapping - * visualization. + * Convenience wrapper around {@link #calculateDeliveryPath} that serializes the result into a + * GeoJSON FeatureCollection suitable for mapping visualization. * * @param records Dispatch records to be fulfilled. * @return GeoJSON payload representing every delivery flight path. * @throws IllegalStateException When the payload cannot be serialized. */ - public String calculateDeliveryPathAsGeoJson( - MedDispatchRecRequest[] records) { + public String calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[] records) { DeliveryPathResponse response = calculateDeliveryPath(records); Map featureCollection = new LinkedHashMap<>(); featureCollection.put("type", "FeatureCollection"); @@ -232,16 +213,13 @@ public class PathFinderService { try { return objectMapper.writeValueAsString(featureCollection); } catch (JsonProcessingException e) { - throw new IllegalStateException( - "Failed to generate GeoJSON payload", - e); + throw new IllegalStateException("Failed to generate GeoJSON payload", e); } } /** - * Group dispatch records by their assigned drone, ensuring every record is - * routed through {@link #findBestDrone(MedDispatchRecRequest)} exactly once - * and discarding invalid entries. + * Group dispatch records by their assigned drone, ensuring every record is routed through + * {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries. * * @param records Dispatch records to be grouped. * @return Map keyed by drone ID with the deliveries it should service. @@ -254,16 +232,14 @@ public class PathFinderService { continue; } String droneId = findBestDrone(record); - assignments - .computeIfAbsent(droneId, id -> new ArrayList<>()) - .add(record); + assignments.computeIfAbsent(droneId, id -> new ArrayList<>()).add(record); } return assignments; } /** - * Choose the best drone for the provided record. Currently that equates to - * picking the closest compatible drone to the delivery location. + * Choose the best drone for the provided record. Currently that equates to picking the closest + * compatible drone to the delivery location. * * @param record Dispatch record that needs fulfillment. * @return Identifier of the drone that should fly the mission. @@ -282,15 +258,14 @@ public class PathFinderService { if (servicePointId == null) { continue; } - LngLat servicePointLocation = servicePointLocations.get( - servicePointId); + LngLat servicePointLocation = servicePointLocations.get(servicePointId); if (servicePointLocation == null) { continue; } - double distance = gpsCalculationService.calculateDistance( - servicePointLocation, - record.delivery()); + double distance = + gpsCalculationService.calculateDistance( + servicePointLocation, record.delivery()); if (distance < bestScore) { bestScore = distance; @@ -298,28 +273,23 @@ public class PathFinderService { } } if (bestDrone == null) { - throw new IllegalStateException( - "No available drone for delivery " + record.id()); + throw new IllegalStateException("No available drone for delivery " + record.id()); } return bestDrone; } /** - * Break a sequence of deliveries into several trips that each respect the - * drone move limit. The deliveries should already be ordered by proximity - * for sensible grouping. + * Break a sequence of deliveries into several trips that each respect the drone move limit. The + * deliveries should already be ordered by proximity for sensible grouping. * - * @param deliveries Deliveries assigned to a drone. - * @param drone Drone that will service the deliveries. + * @param deliveries Deliveries assigned to a drone. + * @param drone Drone that will service the deliveries. * @param servicePoint Starting and ending point of every trip. * @return Partitioned trips with at least one delivery each. - * @throws IllegalStateException If a single delivery exceeds the drone's - * move limit. + * @throws IllegalStateException If a single delivery exceeds the drone's move limit. */ private List> splitTrips( - List deliveries, - Drone drone, - LngLat servicePoint) { + List deliveries, Drone drone, LngLat servicePoint) { List> trips = new ArrayList<>(); List currentTrip = new ArrayList<>(); for (MedDispatchRecRequest delivery : deliveries) { @@ -329,11 +299,11 @@ public class PathFinderService { currentTrip.remove(currentTrip.size() - 1); if (currentTrip.isEmpty()) { throw new IllegalStateException( - "Delivery " + - delivery.id() + - " exceeds drone " + - drone.id() + - " move limit"); + "Delivery " + + delivery.id() + + " exceeds drone " + + drone.id() + + " move limit"); } trips.add(new ArrayList<>(currentTrip)); currentTrip.clear(); @@ -352,20 +322,18 @@ public class PathFinderService { } /** - * Build a single trip for the provided drone, including the entire flight - * path to every delivery and back home. The resulting structure contains the - * {@link DronePath} representation as well as cost and moves consumed. + * Build a single trip for the provided drone, including the entire flight path to every + * delivery and back home. The resulting structure contains the {@link DronePath} representation + * as well as cost and moves consumed. * - * @param drone Drone executing the trip. + * @param drone Drone executing the trip. * @param servicePoint Starting/ending location of the trip. - * @param deliveries Deliveries to include in the trip in execution order. + * @param deliveries Deliveries to include in the trip in execution order. * @return Trip information or {@code null} if no deliveries are provided. * @see DeliveryPathResponse.DronePath */ private TripResult buildTrip( - Drone drone, - LngLat servicePoint, - List deliveries) { + Drone drone, LngLat servicePoint, List deliveries) { if (deliveries == null || deliveries.isEmpty()) { return null; } @@ -390,9 +358,7 @@ public class PathFinderService { moves += toDelivery.moves(); if (i == deliveries.size() - 1) { - PathSegment backHome = computePath( - delivery.delivery(), - servicePoint); + PathSegment backHome = computePath(delivery.delivery(), servicePoint); backHome.appendSkippingStart(flightPath); moves += backHome.moves(); current = servicePoint; @@ -402,9 +368,10 @@ public class PathFinderService { flightPlans.add(new Delivery(delivery.id(), flightPath)); } - float cost = drone.capability().costInitial() + - drone.capability().costFinal() + - (float) (drone.capability().costPerMove() * moves); + float cost = + drone.capability().costInitial() + + drone.capability().costFinal() + + (float) (drone.capability().costPerMove() * moves); DronePath path = new DronePath(drone.parseId(), flightPlans); @@ -412,16 +379,14 @@ public class PathFinderService { } /** - * Estimate the number of moves a prospective trip would need by replaying - * the path calculation without mutating any persistent state. + * Estimate the number of moves a prospective trip would need by replaying the path calculation + * without mutating any persistent state. * * @param servicePoint Trip origin. - * @param deliveries Deliveries that would compose the trip. + * @param deliveries Deliveries that would compose the trip. * @return Total moves required to fly the proposed itinerary. */ - private int estimateTripMoves( - LngLat servicePoint, - List deliveries) { + private int estimateTripMoves(LngLat servicePoint, List deliveries) { if (deliveries.isEmpty()) { return 0; } @@ -437,10 +402,10 @@ public class PathFinderService { } /** - * Build a path between {@code start} and {@code target} by repeatedly moving - * in snapped increments while avoiding restricted zones. + * Build a path between {@code start} and {@code target} by repeatedly moving in snapped + * increments while avoiding restricted zones. * - * @param start Start coordinate. + * @param start Start coordinate. * @param target Destination coordinate. * @return Sequence of visited coordinates and move count. * @see #nextPosition(LngLat, LngLat) @@ -453,8 +418,8 @@ public class PathFinderService { positions.add(start); LngLat current = start; int iterations = 0; - while (!gpsCalculationService.isCloseTo(current, target) && - iterations < MAX_SEGMENT_ITERATIONS) { + while (!gpsCalculationService.isCloseTo(current, target) + && iterations < MAX_SEGMENT_ITERATIONS) { LngLat next = nextPosition(current, target); if (next.isSamePoint(current)) { break; @@ -470,20 +435,18 @@ public class PathFinderService { } /** - * Determine the next position on the path from {@code current} toward - * {@code target}, preferring the snapped angle closest to the desired - * heading that does not infiltrate a restricted region. + * Determine the next position on the path from {@code current} toward {@code target}, + * preferring the snapped angle closest to the desired heading that does not infiltrate a + * restricted region. * * @param current Current coordinate. - * @param target Destination coordinate. - * @return Next admissible coordinate or the original point if none can be - * found. + * @param target Destination coordinate. + * @return Next admissible coordinate or the original point if none can be found. */ private LngLat nextPosition(LngLat current, LngLat target) { - double desiredAngle = Math.toDegrees( - Math.atan2( - target.lat() - current.lat(), - target.lng() - current.lng())); + double desiredAngle = + Math.toDegrees( + Math.atan2(target.lat() - current.lat(), target.lng() - current.lng())); List candidateAngles = buildAngleCandidates(desiredAngle); for (Angle angle : candidateAngles) { LngLat next = gpsCalculationService.nextPosition(current, angle); @@ -495,12 +458,11 @@ public class PathFinderService { } /** - * Build a sequence of candidate angles centered on the desired heading, - * expanding symmetrically clockwise and counter-clockwise to explore - * alternative headings if the primary path is blocked. + * Build a sequence of candidate angles centered on the desired heading, expanding symmetrically + * clockwise and counter-clockwise to explore alternative headings if the primary path is + * blocked. * - * @param desiredAngle Bearing in degrees between current and target - * positions. + * @param desiredAngle Bearing in degrees between current and target positions. * @return Ordered list of candidate snapped angles. * @see Angle#snap(double) */ @@ -532,17 +494,16 @@ public class PathFinderService { } /** - * Representation of a computed path segment wrapping the visited positions - * and the number of moves taken to traverse them. + * Representation of a computed path segment wrapping the visited positions and the number of + * moves taken to traverse them. * * @param positions Ordered coordinates that describe the path. - * @param moves Number of moves consumed by the path. + * @param moves Number of moves consumed by the path. */ private record PathSegment(List positions, int moves) { /** - * Append the positions from this segment to {@code target}, skipping the - * first coordinate as it is already represented by the last coordinate - * in the consumer path. + * Append the positions from this segment to {@code target}, skipping the first coordinate + * as it is already represented by the last coordinate in the consumer path. * * @param target Mutable list to append to. */ @@ -554,9 +515,8 @@ public class PathFinderService { } /** - * Bundle containing the calculated {@link DronePath}, total moves and - * financial cost for a single trip. + * Bundle containing the calculated {@link DronePath}, total moves and financial cost for a + * single trip. */ - private record TripResult(DronePath path, int moves, float cost) { - } + private record TripResult(DronePath path, int moves, float cost) {} } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java index a88f6b7..3039dc1 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/util/AttrComparator.java @@ -1,22 +1,21 @@ package io.github.js0ny.ilp_coursework.util; -import java.math.BigDecimal; - import com.fasterxml.jackson.databind.JsonNode; +import java.math.BigDecimal; + /** * Comparator for attribute values in {@code JsonNode}. * - * This is a helper for dynamic querying. + *

This is a helper for dynamic querying. */ public class AttrComparator { /** - * Helper for dynamic querying, to compare the json value with given value in - * {@code String}. + * Helper for dynamic querying, to compare the json value with given value in {@code String}. * - * @param node The {@code JsonNode} to be compared + * @param node The {@code JsonNode} to be compared * @param attrVal The Value passed, in {@code String} - * @param op The comparison operator + * @param op The comparison operator * @return {@code true} if given values are equal, otherwise false. */ public static boolean isValueMatched(JsonNode node, String attrVal, AttrOperator op) { @@ -46,9 +45,9 @@ public class AttrComparator { return switch (op) { case EQ -> nodeVal.equals(attrVal); default -> !nodeVal.equals(attrVal); - // case NE -> !nodeVal.equals(attrVal); - // case GT -> !nodeVal.equals(attrVal);// > 0; - // case LT -> !nodeVal.equals(attrVal);// < 0; + // case NE -> !nodeVal.equals(attrVal); + // case GT -> !nodeVal.equals(attrVal);// > 0; + // case LT -> !nodeVal.equals(attrVal);// < 0; }; } @@ -56,9 +55,9 @@ public class AttrComparator { return switch (op) { case EQ -> nodeVal == attrVal; default -> nodeVal != attrVal; - // case NE -> nodeVal != attrVal; - // case GT -> !nodeVal && attrVal; // false < true - // case LT -> nodeVal && !attrVal; // true > false + // case NE -> nodeVal != attrVal; + // case GT -> !nodeVal && attrVal; // false < true + // case LT -> nodeVal && !attrVal; // true > false }; } } diff --git a/src/test/java/io/github/js0ny/ilp_coursework/ActuatorHealthTest.java b/src/test/java/io/github/js0ny/ilp_coursework/ActuatorHealthTest.java index a00efc2..b455ae2 100644 --- a/src/test/java/io/github/js0ny/ilp_coursework/ActuatorHealthTest.java +++ b/src/test/java/io/github/js0ny/ilp_coursework/ActuatorHealthTest.java @@ -1,5 +1,10 @@ package io.github.js0ny.ilp_coursework; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -7,23 +12,18 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @SpringBootTest @AutoConfigureMockMvc public class ActuatorHealthTest { - @Autowired - private MockMvc mockMvc; + @Autowired private MockMvc mockMvc; @Test @DisplayName("GET /actuator/health -> 200 OK") void getActuator_shouldReturn200AndON() throws Exception { String endpoint = "/actuator/health"; - String expected = """ + String expected = + """ { "status": "UP" } diff --git a/src/test/java/io/github/js0ny/ilp_coursework/IlpCourseworkApplicationTests.java b/src/test/java/io/github/js0ny/ilp_coursework/IlpCourseworkApplicationTests.java index 25e3b6b..6f3a15d 100644 --- a/src/test/java/io/github/js0ny/ilp_coursework/IlpCourseworkApplicationTests.java +++ b/src/test/java/io/github/js0ny/ilp_coursework/IlpCourseworkApplicationTests.java @@ -6,8 +6,6 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class IlpCourseworkApplicationTests { - @Test - void contextLoads() { - } - + @Test + void contextLoads() {} } 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 abd310f..fc0c886 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 @@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; + import io.github.js0ny.ilp_coursework.data.common.Angle; import io.github.js0ny.ilp_coursework.data.common.LngLat; import io.github.js0ny.ilp_coursework.data.common.Region; @@ -15,7 +16,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; @@ -26,17 +27,16 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.List; + @WebMvcTest(ApiController.class) public class ApiControllerTest { - @Autowired - private MockMvc mockMvc; + @Autowired private MockMvc mockMvc; - @Autowired - private ObjectMapper objectMapper; + @Autowired private ObjectMapper objectMapper; - @MockitoBean - private GpsCalculationService service; + @MockitoBean private GpsCalculationService service; @Nested @DisplayName("GET /uid") @@ -59,21 +59,19 @@ public class ApiControllerTest { @Test @DisplayName("POST /distanceTo -> 200 OK") - void getDistance_shouldReturn200AndDistance_whenCorrectInput() - throws Exception { + void getDistance_shouldReturn200AndDistance_whenCorrectInput() 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)) - ).thenReturn(expected); - var mock = mockMvc.perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(req)) - ); + when(service.calculateDistance(any(LngLat.class), any(LngLat.class))) + .thenReturn(expected); + var mock = + mockMvc.perform( + post(endpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))); mock.andExpect(status().isOk()); mock.andExpect(content().string(String.valueOf(expected))); @@ -83,43 +81,33 @@ public class ApiControllerTest { @DisplayName("POST /distanceTo -> 400 Bad Request: Missing Field") void getDistance_shouldReturn400_whenMissingField() throws Exception { String endpoint = "/api/v1/distanceTo"; - String req = """ - { - "position1": { - "lng": 3.0, - "lat": 4.0 + String req = + """ + { + "position1": { + "lng": 3.0, + "lat": 4.0 + } } - } - """; - when( - service.calculateDistance(any(LngLat.class), isNull()) - ).thenThrow(new NullPointerException()); - mockMvc - .perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(req) - ) - .andExpect(status().isBadRequest()); + """; + when(service.calculateDistance(any(LngLat.class), isNull())) + .thenThrow(new NullPointerException()); + mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req)) + .andExpect(status().isBadRequest()); } @Test @DisplayName("POST /distanceTo -> 400 Bad Request: Semantic errors") void getDistance_shouldReturn400_whenInvalidInput() throws Exception { String endpoint = "/api/v1/distanceTo"; - String req = """ - { "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } } - """; - when( - service.calculateDistance(any(LngLat.class), isNull()) - ).thenThrow(new NullPointerException()); - mockMvc - .perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(req) - ) - .andExpect(status().isBadRequest()); + String req = + """ + { "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } } + """; + when(service.calculateDistance(any(LngLat.class), isNull())) + .thenThrow(new NullPointerException()); + mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req)) + .andExpect(status().isBadRequest()); } } @@ -129,21 +117,18 @@ public class ApiControllerTest { @Test @DisplayName("POST /isCloseTo -> 200 OK") - void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() - throws Exception { + void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() 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)) - ).thenReturn(expected); - var mock = mockMvc.perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(req)) - ); + when(service.isCloseTo(any(LngLat.class), any(LngLat.class))).thenReturn(expected); + var mock = + mockMvc.perform( + post(endpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))); mock.andExpect(status().isOk()); mock.andExpect(content().string(String.valueOf(expected))); @@ -151,39 +136,32 @@ public class ApiControllerTest { @Test @DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ") - void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() - throws Exception { + void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() throws Exception { // json without a bracket - String malformedJson = """ - { - "position1": { "lng": 0.0, "lat": 3.0 } - """; - mockMvc - .perform( - post("/api/v1/isCloseTo") - .contentType(MediaType.APPLICATION_JSON) - .content(malformedJson) - ) - .andExpect(status().isBadRequest()); + String malformedJson = + """ + { + "position1": { "lng": 0.0, "lat": 3.0 } + """; + mockMvc.perform( + post("/api/v1/isCloseTo") + .contentType(MediaType.APPLICATION_JSON) + .content(malformedJson)) + .andExpect(status().isBadRequest()); } @Test @DisplayName("POST /isCloseTo -> 400 Bad Request: Semantic errors") void getIsCloseTo_shouldReturn400_whenInvalidInput() throws Exception { String endpoint = "/api/v1/isCloseTo"; - String req = """ - { "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } } + String req = + """ + { "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } } """; - when( - service.calculateDistance(any(LngLat.class), isNull()) - ).thenThrow(new NullPointerException()); - mockMvc - .perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(req) - ) - .andExpect(status().isBadRequest()); + when(service.calculateDistance(any(LngLat.class), isNull())) + .thenThrow(new NullPointerException()); + mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req)) + .andExpect(status().isBadRequest()); } } @@ -195,73 +173,59 @@ public class ApiControllerTest { @Test @DisplayName("POST /nextPosition -> 200 OK") - void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() - throws Exception { + void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() 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), any(Angle.class)) - ).thenReturn(expected); - var mock = mockMvc.perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(req)) - ); + when(service.nextPosition(any(LngLat.class), any(Angle.class))).thenReturn(expected); + var mock = + mockMvc.perform( + post(endpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))); mock.andExpect(status().isOk()); - mock.andExpect( - content().json(objectMapper.writeValueAsString(expected)) - ); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field") - void getNextPosition_shouldReturn400_whenKeyNameError() - throws Exception { + void getNextPosition_shouldReturn400_whenKeyNameError() throws Exception { // "position" should be "start" - String malformedJson = """ - { - "position": { "lng": 0.0, "lat": 3.0 }, - "angle": 180 - } - """; - when(service.nextPosition(isNull(), any(Angle.class))).thenThrow( - new NullPointerException() - ); - mockMvc - .perform( - post("/api/v1/nextPosition") - .contentType(MediaType.APPLICATION_JSON) - .content(malformedJson) - ) - .andExpect(MockMvcResultMatchers.status().isBadRequest()); + String malformedJson = + """ + { + "position": { "lng": 0.0, "lat": 3.0 }, + "angle": 180 + } + """; + when(service.nextPosition(isNull(), any(Angle.class))) + .thenThrow(new NullPointerException()); + mockMvc.perform( + post("/api/v1/nextPosition") + .contentType(MediaType.APPLICATION_JSON) + .content(malformedJson)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); } @Test @DisplayName("POST /nextPosition -> 400 Bad Request: Semantic errors") - void getNextPosition_shouldReturn400_whenInvalidInput() - throws Exception { + void getNextPosition_shouldReturn400_whenInvalidInput() throws Exception { String endpoint = "/api/v1/nextPosition"; - String req = """ - { - "start": { - "lng": -3.192473, - "lat": 55.946233 - }, - "angle": 900 - } - """; - when( - service.calculateDistance(any(LngLat.class), isNull()) - ).thenThrow(new NullPointerException()); - mockMvc - .perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(req) - ) - .andExpect(status().isBadRequest()); + String req = + """ + { + "start": { + "lng": -3.192473, + "lat": 55.946233 + }, + "angle": 900 + } + """; + when(service.calculateDistance(any(LngLat.class), isNull())) + .thenThrow(new NullPointerException()); + mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req)) + .andExpect(status().isBadRequest()); } } @@ -271,30 +235,27 @@ public class ApiControllerTest { @Test @DisplayName("POST /isInRegion -> 200 OK") - void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() - throws Exception { + void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() 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) - ) - ); + 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 req = new RegionCheckRequest(position, region); - when( - service.checkIsInRegion(any(LngLat.class), any(Region.class)) - ).thenReturn(expected); - var mock = mockMvc.perform( - post(endpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(req)) - ); + when(service.checkIsInRegion(any(LngLat.class), any(Region.class))) + .thenReturn(expected); + var mock = + mockMvc.perform( + post(endpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))); mock.andExpect(status().isOk()); mock.andExpect(content().string(String.valueOf(expected))); @@ -302,53 +263,44 @@ public class ApiControllerTest { @Test @DisplayName( - "POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion" - ) - void getIsInRegion_shouldReturn400_whenPassingIllegalArguments() - throws Exception { + "POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to" + + " isInRegion") + void getIsInRegion_shouldReturn400_whenPassingIllegalArguments() 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)) - ).thenThrow(new IllegalArgumentException("Region is not closed.")); - mockMvc - .perform( - post("/api/v1/isInRegion") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - ) - .andExpect(status().isBadRequest()); + when(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()); } @Test @DisplayName( - "POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion" - ) - void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices() - throws Exception { + "POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to" + + " isInRegion") + void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices() 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) - ) - ); + 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))); var request = new RegionCheckRequest(position, region); - when( - 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()); + when(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()); } } } diff --git a/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java b/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java index a9ae4f8..a377da7 100644 --- a/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java +++ b/src/test/java/io/github/js0ny/ilp_coursework/controller/DroneControllerTest.java @@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + import io.github.js0ny.ilp_coursework.data.common.DroneCapability; import io.github.js0ny.ilp_coursework.data.common.LngLat; import io.github.js0ny.ilp_coursework.data.external.Drone; @@ -17,10 +18,7 @@ import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService; import io.github.js0ny.ilp_coursework.service.DroneInfoService; import io.github.js0ny.ilp_coursework.service.PathFinderService; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; -import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -31,22 +29,23 @@ import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; + @WebMvcTest(DroneController.class) public class DroneControllerTest { - @Autowired - private MockMvc mockMvc; + @Autowired private MockMvc mockMvc; private ObjectMapper objectMapper; - @MockitoBean - private DroneInfoService droneInfoService; + @MockitoBean private DroneInfoService droneInfoService; - @MockitoBean - private DroneAttrComparatorService droneAttrComparatorService; + @MockitoBean private DroneAttrComparatorService droneAttrComparatorService; - @MockitoBean - private PathFinderService pathFinderService; + @MockitoBean private PathFinderService pathFinderService; @BeforeEach void setUp() { @@ -66,14 +65,11 @@ public class DroneControllerTest { throws Exception { String endpoint = API_ENDPOINT_BASE + "true"; List expected = List.of("1", "5", "8", "9"); - when( - droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); - var mock = mockMvc.perform( - get(endpoint).contentType(MediaType.APPLICATION_JSON)); + when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); + var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)); mock.andExpect(status().isOk()); - mock.andExpect( - content().json(objectMapper.writeValueAsString(expected))); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @@ -82,23 +78,18 @@ public class DroneControllerTest { throws Exception { String endpoint = API_ENDPOINT_BASE + "false"; List expected = List.of("2", "3", "4", "6", "7", "10"); - when( - droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); - var mock = mockMvc.perform( - get(endpoint).contentType(MediaType.APPLICATION_JSON)); + when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected); + var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)); mock.andExpect(status().isOk()); - mock.andExpect( - content().json(objectMapper.writeValueAsString(expected))); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("-> 400 Bad Request") - void getDronesWithCooling_shouldReturn400_whenStateIsInvalid() - throws Exception { + void getDronesWithCooling_shouldReturn400_whenStateIsInvalid() throws Exception { String endpoint = API_ENDPOINT_BASE + "invalid"; - mockMvc.perform( - get(endpoint).contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()); } } @@ -111,33 +102,29 @@ public class DroneControllerTest { @Test @DisplayName("-> 200 OK") - void getDroneDetails_shouldReturn200AndJson_whenCorrectInput() - throws Exception { - Drone expected = new Drone("Drone 1", "1", - new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f)); + void getDroneDetails_shouldReturn200AndJson_whenCorrectInput() throws Exception { + Drone expected = + new Drone( + "Drone 1", + "1", + new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f)); String endpoint = API_ENDPOINT_BASE + "1"; - when( - droneInfoService.droneDetail(anyString())).thenReturn(expected); - var mock = mockMvc.perform( - get(endpoint).contentType(MediaType.APPLICATION_JSON)); + when(droneInfoService.droneDetail(anyString())).thenReturn(expected); + var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)); mock.andExpect(status().isOk()); - mock.andExpect( - content().json(objectMapper.writeValueAsString(expected))); + mock.andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("-> 404 Not Found") - void getDroneDetails_shouldReturn404_whenDroneNotFound() - throws Exception { + void getDroneDetails_shouldReturn404_whenDroneNotFound() throws Exception { String endpoint = API_ENDPOINT_BASE + "invalidDroneId"; - when( - droneInfoService.droneDetail(anyString())).thenThrow(new IllegalArgumentException()); - mockMvc.perform( - get(endpoint).contentType(MediaType.APPLICATION_JSON)) + when(droneInfoService.droneDetail(anyString())) + .thenThrow(new IllegalArgumentException()); + mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); } - } @Nested @@ -148,64 +135,64 @@ public class DroneControllerTest { @Test @DisplayName("capacity = 8 -> 200 OK") - void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8() - throws Exception { + void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8() throws Exception { String attrName = "capacity"; String attrVal = "8"; List expected = List.of("2", "4", "7", "9"); when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) .thenReturn(expected); - mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("heating = true -> 200 OK") - void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue() - throws Exception { + void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue() throws Exception { String attrName = "heating"; String attrVal = "true"; List expected = List.of("1", "2", "4", "5", "6", "7", "9"); when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) .thenReturn(expected); - mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("cooling = false -> 200 OK") - void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse() - throws Exception { + void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse() throws Exception { String attrName = "cooling"; String attrVal = "false"; List expected = List.of("2", "3", "4", "6", "7", "10"); when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) .thenReturn(expected); - mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("maxMoves = 1000 -> 200 OK") - void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000() - throws Exception { + void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000() throws Exception { String attrName = "maxMoves"; String attrVal = "1000"; List expected = List.of("2", "4", "7", "9"); when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) .thenReturn(expected); - mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -220,8 +207,9 @@ public class DroneControllerTest { when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal)) .thenReturn(expected); - mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal) - .contentType(MediaType.APPLICATION_JSON)) + mockMvc.perform( + get(API_ENDPOINT_BASE + attrName + "/" + attrVal) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -243,31 +231,34 @@ public class DroneControllerTest { AttrQueryRequest[] requestBody = {req1, req2, req3}; List expected = List.of("8"); - when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + when(droneAttrComparatorService.dronesSatisfyingAttributes( + any(AttrQueryRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("GT LT -> 200 OK") - void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions() - throws Exception { + void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions() throws Exception { AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8"); AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000"); AttrQueryRequest[] requestBody = {req1, req2}; List expected = List.of("5", "10"); - when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + when(droneAttrComparatorService.dronesSatisfyingAttributes( + any(AttrQueryRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -281,32 +272,35 @@ public class DroneControllerTest { AttrQueryRequest[] requestBody = {req1, req2}; List expected = List.of(); - when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + when(droneAttrComparatorService.dronesSatisfyingAttributes( + any(AttrQueryRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @Test @DisplayName("GT LT EQ -> 200 OK") - void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions() - throws Exception { + void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions() throws Exception { AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8"); AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000"); AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true"); AttrQueryRequest[] requestBody = {req1, req2, req3}; List expected = List.of("5"); - when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class))) + when(droneAttrComparatorService.dronesSatisfyingAttributes( + any(AttrQueryRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -324,16 +318,23 @@ public class DroneControllerTest { throws Exception { var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); var delivery = new LngLat(-3.00, 55.121); - var record = new MedDispatchRecRequest(123, LocalDate.parse("2025-12-22"), LocalTime.parse("14:30"), reqs, delivery); + var record = + new MedDispatchRecRequest( + 123, + LocalDate.parse("2025-12-22"), + LocalTime.parse("14:30"), + reqs, + delivery); MedDispatchRecRequest[] requestBody = {record}; List expected = List.of("1", "2", "6", "7", "9"); when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -342,24 +343,30 @@ public class DroneControllerTest { @DisplayName("Treat Null as False (Cooling) -> 200 OK") void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenCoolingIsNull() throws Exception { - var requestMap = Map.of( - "id", 123, - "date", "2025-12-22", - "time", "14:30", - "requirements", Map.of( - "capacity", 0.75, - "heating", true, - "maxCost", 13.5 - ) - ); + var requestMap = + Map.of( + "id", + 123, + "date", + "2025-12-22", + "time", + "14:30", + "requirements", + Map.of( + "capacity", 0.75, + "heating", true, + "maxCost", 13.5)); List expected = List.of("1", "2", "6", "7", "9"); when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(new Object[]{requestMap}))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content( + objectMapper.writeValueAsString( + new Object[] {requestMap}))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } @@ -370,11 +377,23 @@ public class DroneControllerTest { throws Exception { var reqs1 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); var delivery1 = new LngLat(-3.00, 55.121); - var record1 = new MedDispatchRecRequest(123, LocalDate.parse("2025-12-22"), LocalTime.parse("14:30"), reqs1, delivery1); + var record1 = + new MedDispatchRecRequest( + 123, + LocalDate.parse("2025-12-22"), + LocalTime.parse("14:30"), + reqs1, + delivery1); var reqs2 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f); var delivery2 = new LngLat(-3.00, 55.121); - var record2 = new MedDispatchRecRequest(456, LocalDate.parse("2025-12-25"), LocalTime.parse("11:30"), reqs2, delivery2); + var record2 = + new MedDispatchRecRequest( + 456, + LocalDate.parse("2025-12-25"), + LocalTime.parse("11:30"), + reqs2, + delivery2); MedDispatchRecRequest[] requestBody = {record1, record2}; List expected = List.of("2", "7", "9"); @@ -382,11 +401,12 @@ public class DroneControllerTest { when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class))) .thenReturn(expected); - mockMvc.perform(post(API_ENDPOINT) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(requestBody))) + mockMvc.perform( + post(API_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) .andExpect(status().isOk()) .andExpect(content().json(objectMapper.writeValueAsString(expected))); } } -} \ No newline at end of file +} diff --git a/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java b/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java index 16a39cd..6595e66 100644 --- a/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java +++ b/src/test/java/io/github/js0ny/ilp_coursework/service/DroneInfoServiceTest.java @@ -11,11 +11,7 @@ import io.github.js0ny.ilp_coursework.data.common.TimeWindow; import io.github.js0ny.ilp_coursework.data.external.Drone; import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; -import java.net.URI; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -26,11 +22,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; +import java.net.URI; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + @ExtendWith(MockitoExtension.class) public class DroneInfoServiceTest { - @Mock - private RestTemplate restTemplate; + @Mock private RestTemplate restTemplate; private DroneInfoService droneInfoService; @@ -44,9 +45,9 @@ public class DroneInfoServiceTest { private Drone[] getMockDrones() { return new Drone[] { - new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), - new Drone("Drone 2", "2", new DroneCapability(false, true, 20, 2000, 2, 2, 2)), - new Drone("Drone 3", "3", new DroneCapability(false, false, 30, 3000, 3, 3, 3)) + new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone("Drone 2", "2", new DroneCapability(false, true, 20, 2000, 2, 2, 2)), + new Drone("Drone 3", "3", new DroneCapability(false, false, 30, 3000, 3, 3, 3)) }; } @@ -134,30 +135,46 @@ public class DroneInfoServiceTest { @DisplayName("dronesMatchesRequirements(MedDispatchRecRequest[]) tests") class DronesMatchesRequirementsTests { private ServicePointDrones[] getMockServicePointDrones() { - TimeWindow[] timeWindows = { new TimeWindow(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(17, 0)) }; + TimeWindow[] timeWindows = { + new TimeWindow(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(17, 0)) + }; DroneAvailability drone1Avail = new DroneAvailability("1", timeWindows); - ServicePointDrones spd = new ServicePointDrones(1, new DroneAvailability[] { drone1Avail }); - return new ServicePointDrones[] { spd }; + ServicePointDrones spd = + new ServicePointDrones(1, new DroneAvailability[] {drone1Avail}); + return new ServicePointDrones[] {spd}; } @Test @DisplayName("Should return drones matching a single requirement") void dronesMatchesRequirements_shouldReturnMatchingDrones_forSingleRequirement() { // Arrange - var drones = new Drone[] { - new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), - new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) - }; - when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones); - when(restTemplate.getForObject(URI.create(baseUrl + "drones-for-service-points"), - ServicePointDrones[].class)).thenReturn(getMockServicePointDrones()); + var drones = + new Drone[] { + new Drone( + "Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)), + new Drone( + "Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) + }; + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(drones); + when(restTemplate.getForObject( + URI.create(baseUrl + "drones-for-service-points"), + ServicePointDrones[].class)) + .thenReturn(getMockServicePointDrones()); var requirement = new MedDispatchRecRequest.MedRequirement(8, true, false, 100); - var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0), - requirement, new LngLat(0, 0)); + var record = + new MedDispatchRecRequest( + 1, + LocalDate.now().with(DayOfWeek.MONDAY), + LocalTime.of(10, 0), + requirement, + new LngLat(0, 0)); // Act - List result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record }); + List result = + droneInfoService.dronesMatchesRequirements( + new MedDispatchRecRequest[] {record}); // Assert assertThat(result).containsExactly("1"); @@ -167,19 +184,30 @@ public class DroneInfoServiceTest { @DisplayName("Should return empty list if no drones match") void dronesMatchesRequirements_shouldReturnEmptyList_whenNoDronesMatch() { // Arrange - var drones = new Drone[] { - new Drone("Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)), - new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) - }; - when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones); + var drones = + new Drone[] { + new Drone( + "Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)), + new Drone( + "Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2)) + }; + when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)) + .thenReturn(drones); // No need to mock drones-for-service-points as it won't be called var requirement = new MedDispatchRecRequest.MedRequirement(10, true, false, 100); - var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0), - requirement, new LngLat(0, 0)); + var record = + new MedDispatchRecRequest( + 1, + LocalDate.now().with(DayOfWeek.MONDAY), + LocalTime.of(10, 0), + requirement, + new LngLat(0, 0)); // Act - List result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record }); + List result = + droneInfoService.dronesMatchesRequirements( + new MedDispatchRecRequest[] {record}); // Assert assertThat(result).isEmpty(); @@ -194,7 +222,8 @@ public class DroneInfoServiceTest { // Act List resultNull = droneInfoService.dronesMatchesRequirements(null); - List resultEmpty = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]); + List resultEmpty = + droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]); // Assert assertThat(resultNull).containsExactly("1", "2", "3"); 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 fe9ce8c..2bbe52f 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 @@ -7,12 +7,14 @@ import static org.assertj.core.api.AssertionsForClassTypes.within; import io.github.js0ny.ilp_coursework.data.common.Angle; 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; +import java.util.List; + public class GpsCalculationServiceTest { private static final double STEP = 0.00015; @@ -114,9 +116,7 @@ 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); @@ -160,20 +160,12 @@ 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); Angle angle = new Angle(90); @@ -182,20 +174,12 @@ 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); Angle angle = new Angle(180); @@ -205,20 +189,12 @@ 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); Angle angle = new Angle(270); @@ -228,20 +204,12 @@ 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); Angle angle = new Angle(45); @@ -252,47 +220,37 @@ 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)); } @Nested - @DisplayName( - "Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean" - ) + @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); @@ -303,10 +261,7 @@ 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); } @@ -315,10 +270,7 @@ 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); } @@ -326,18 +278,17 @@ 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); @@ -347,15 +298,14 @@ 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); @@ -366,10 +316,7 @@ 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); } @@ -378,10 +325,7 @@ 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); } @@ -390,10 +334,7 @@ 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); } @@ -402,10 +343,7 @@ 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); } @@ -414,10 +352,7 @@ 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); } @@ -425,40 +360,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) - ) - ); - assertThatThrownBy(() -> { - service.checkIsInRegion(position, region); - }) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Region is not closed."); + 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."); } @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) - ) - ); - assertThatThrownBy(() -> { - service.checkIsInRegion(position, region); - }) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Region is not closed."); + 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."); } @Test @@ -466,11 +401,12 @@ public class GpsCalculationServiceTest { void isInRegion_shouldThrowExceptions_whenListIsEmpty() { var position = new LngLat(2.0, 2.0); var region = new Region("rectangle", List.of()); - assertThatThrownBy(() -> { - service.checkIsInRegion(position, region); - }) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Region is not closed."); + assertThatThrownBy( + () -> { + service.checkIsInRegion(position, region); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Region is not closed."); } } } diff --git a/src/test/java/io/github/js0ny/ilp_coursework/service/PathFinderServiceTest.java b/src/test/java/io/github/js0ny/ilp_coursework/service/PathFinderServiceTest.java index 623ff5d..3bf6268 100644 --- a/src/test/java/io/github/js0ny/ilp_coursework/service/PathFinderServiceTest.java +++ b/src/test/java/io/github/js0ny/ilp_coursework/service/PathFinderServiceTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import io.github.js0ny.ilp_coursework.data.common.DroneAvailability; import io.github.js0ny.ilp_coursework.data.common.DroneCapability; import io.github.js0ny.ilp_coursework.data.common.LngLat; @@ -18,17 +19,19 @@ import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest.MedRequirement; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + import java.io.IOException; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalTime; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class PathFinderServiceTest { @@ -37,8 +40,7 @@ class PathFinderServiceTest { private static final LngLat SERVICE_POINT_COORD = new LngLat(0.0, 0.0); private final ObjectMapper mapper = new ObjectMapper(); - @Mock - private DroneInfoService droneInfoService; + @Mock private DroneInfoService droneInfoService; private PathFinderService pathFinderService; @@ -46,66 +48,38 @@ class PathFinderServiceTest { void setUpPathFinder() { GpsCalculationService gpsCalculationService = new GpsCalculationService(); - DroneCapability capability = new DroneCapability( - false, - true, - 5.0f, - 10, - 0.1f, - 0.5f, - 0.5f - ); + DroneCapability capability = new DroneCapability(false, true, 5.0f, 10, 0.1f, 0.5f, 0.5f); Drone drone = new Drone("Test Drone", DRONE_ID, capability); - ServicePoint servicePoint = new ServicePoint( - "Test Point", - 1, - new LngLatAlt( - SERVICE_POINT_COORD.lng(), - SERVICE_POINT_COORD.lat(), - 50.0 - ) - ); - DroneAvailability availability = new DroneAvailability( - DRONE_ID, - new TimeWindow[] { - new TimeWindow( - DayOfWeek.MONDAY, - LocalTime.MIDNIGHT, - LocalTime.MAX - ), - } - ); - ServicePointDrones servicePointDrones = new ServicePointDrones( - servicePoint.id(), - new DroneAvailability[] { availability } - ); + ServicePoint servicePoint = + new ServicePoint( + "Test Point", + 1, + new LngLatAlt(SERVICE_POINT_COORD.lng(), SERVICE_POINT_COORD.lat(), 50.0)); + DroneAvailability availability = + new DroneAvailability( + DRONE_ID, + new TimeWindow[] { + new TimeWindow(DayOfWeek.MONDAY, LocalTime.MIDNIGHT, LocalTime.MAX), + }); + ServicePointDrones servicePointDrones = + new ServicePointDrones(servicePoint.id(), new DroneAvailability[] {availability}); when(droneInfoService.fetchAllDrones()).thenReturn(List.of(drone)); - when(droneInfoService.fetchServicePoints()).thenReturn( - List.of(servicePoint) - ); - when(droneInfoService.fetchDronesForServicePoints()).thenReturn( - List.of(servicePointDrones) - ); - when(droneInfoService.fetchRestrictedAreas()).thenReturn( - Collections.emptyList() - ); - when(droneInfoService.droneMatchesRequirement(any(), any())).thenReturn( - true - ); - pathFinderService = new PathFinderService( - gpsCalculationService, - droneInfoService - ); + when(droneInfoService.fetchServicePoints()).thenReturn(List.of(servicePoint)); + when(droneInfoService.fetchDronesForServicePoints()) + .thenReturn(List.of(servicePointDrones)); + when(droneInfoService.fetchRestrictedAreas()) + .thenReturn(Collections.emptyList()); + when(droneInfoService.droneMatchesRequirement(any(), any())).thenReturn(true); + pathFinderService = new PathFinderService(gpsCalculationService, droneInfoService); } @Test void calculateDeliveryPath_shouldStayWithinSingleTripBudget() { MedDispatchRecRequest request = createSampleRequest(); - DeliveryPathResponse response = pathFinderService.calculateDeliveryPath( - new MedDispatchRecRequest[] { request } - ); + DeliveryPathResponse response = + pathFinderService.calculateDeliveryPath(new MedDispatchRecRequest[] {request}); assertThat(response.totalMoves()).isGreaterThan(0); assertThat(response.totalMoves()).isLessThanOrEqualTo(10); @@ -115,24 +89,18 @@ class PathFinderServiceTest { assertThat(dronePath.deliveries()).hasSize(1); var recordedPath = dronePath.deliveries().get(0).flightPath(); assertThat(recordedPath.get(0)).isEqualTo(SERVICE_POINT_COORD); - assertThat( - samePoint( - recordedPath.get(recordedPath.size() - 1), - SERVICE_POINT_COORD - ) - ) - .isTrue(); + assertThat(samePoint(recordedPath.get(recordedPath.size() - 1), SERVICE_POINT_COORD)) + .isTrue(); assertThat(hasHoverAt(recordedPath, request.delivery())).isTrue(); } @Test - void calculateDeliveryPathAsGeoJson_shouldReturnFeatureCollection() - throws IOException { + void calculateDeliveryPathAsGeoJson_shouldReturnFeatureCollection() throws IOException { MedDispatchRecRequest request = createSampleRequest(); - String geoJson = pathFinderService.calculateDeliveryPathAsGeoJson( - new MedDispatchRecRequest[] { request } - ); + String geoJson = + pathFinderService.calculateDeliveryPathAsGeoJson( + new MedDispatchRecRequest[] {request}); JsonNode root = mapper.readTree(geoJson); assertThat(root.get("type").asText()).isEqualTo("FeatureCollection"); @@ -151,20 +119,16 @@ class PathFinderServiceTest { double startLng = coordinates.get(0).get(0).asDouble(); double startLat = coordinates.get(0).get(1).asDouble(); - assertThat( - samePoint(new LngLat(startLng, startLat), SERVICE_POINT_COORD) - ) - .isTrue(); + assertThat(samePoint(new LngLat(startLng, startLat), SERVICE_POINT_COORD)).isTrue(); } private MedDispatchRecRequest createSampleRequest() { return new MedDispatchRecRequest( - 101, - LocalDate.of(2025, 1, 6), - LocalTime.of(12, 0), - new MedRequirement(0.5f, false, true, 50.0f), - new LngLat(SERVICE_POINT_COORD.lng() + 0.0003, SERVICE_POINT_COORD.lat()) - ); + 101, + LocalDate.of(2025, 1, 6), + LocalTime.of(12, 0), + new MedRequirement(0.5f, false, true, 50.0f), + new LngLat(SERVICE_POINT_COORD.lng() + 0.0003, SERVICE_POINT_COORD.lat())); } private boolean hasHoverAt(List path, LngLat target) { @@ -178,9 +142,6 @@ class PathFinderServiceTest { private boolean samePoint(LngLat a, LngLat b) { double threshold = 1e-9; - return ( - Math.abs(a.lng() - b.lng()) < threshold && - Math.abs(a.lat() - b.lat()) < threshold - ); + return (Math.abs(a.lng() - b.lng()) < threshold && Math.abs(a.lat() - b.lat()) < threshold); } }