diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru new file mode 100644 index 0000000..2d0ff96 --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru @@ -0,0 +1,41 @@ +meta { + name: Complex + type: http + seq: 3 +} + +post { + url: {{API_BASE}}/queryAvailableDrones + body: json + auth: inherit +} + +body:json { + [ + { + "id": 123, + "date": "2025-12-22", + "time": "14:30", + "requirements": { + "capacity": 0.75, + "heating": true, + "maxCost": 13.5 + } + }, + { + "id": 456, + "date": "2025-12-25", + "time": "11:30", + "requirements": { + "capacity": 0.75, + "heating": true, + "maxCost": 13.5 + } + } + ] +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Example.bru b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru new file mode 100644 index 0000000..9de1eaa --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru @@ -0,0 +1,32 @@ +meta { + name: Example + type: http + seq: 1 +} + +post { + url: {{API_BASE}}/queryAvailableDrones + body: json + auth: inherit +} + +body:json { + [ + { + "id": 123, + "date": "2025-12-22", + "time": "14:30", + "requirements": { + "capacity": 0.75, + "cooling": false, + "heating": true, + "maxCost": 13.5 + } + } + ] +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru new file mode 100644 index 0000000..7fd90ff --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru @@ -0,0 +1,31 @@ +meta { + name: Treat Null as False (Cooling) + type: http + seq: 2 +} + +post { + url: {{API_BASE}}/queryAvailableDrones + body: json + auth: inherit +} + +body:json { + [ + { + "id": 123, + "date": "2025-12-22", + "time": "14:30", + "requirements": { + "capacity": 0.75, + "heating": true, + "maxCost": 13.5 + } + } + ] +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/ilp-cw-api/[POST] queryAvailableDrones/folder.bru b/ilp-cw-api/[POST] queryAvailableDrones/folder.bru new file mode 100644 index 0000000..ec363e1 --- /dev/null +++ b/ilp-cw-api/[POST] queryAvailableDrones/folder.bru @@ -0,0 +1,8 @@ +meta { + name: [POST] queryAvailableDrones + seq: 5 +} + +auth { + mode: inherit +} 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 4dabb58..4beea49 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 @@ -4,7 +4,7 @@ import io.github.js0ny.ilp_coursework.data.AttrComparatorDto; import io.github.js0ny.ilp_coursework.data.DeliveryPathDto; import io.github.js0ny.ilp_coursework.data.DroneDto; import io.github.js0ny.ilp_coursework.data.DronePathDto; -import io.github.js0ny.ilp_coursework.data.MedDispathRecDto; +import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto; import io.github.js0ny.ilp_coursework.service.DroneInfoService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -56,7 +56,7 @@ public class DroneController { * * @param id The id of the drone to be queried. * @return 200 with {@link DroneDto}-style json if success, 404 if {@code id} - * not found, 400 otherwise + * not found, 400 otherwise */ @GetMapping("/droneDetails/{id}") public ResponseEntity getDroneDetail(@PathVariable String id) { @@ -89,17 +89,17 @@ public class DroneController { } @PostMapping("/queryAvailableDrones") - public String[] queryAvailableDrones(@RequestBody MedDispathRecDto[] records) { + public String[] queryAvailableDrones(@RequestBody MedDispatchRecDto[] records) { return droneService.dronesMatchesRequirements(records); } @PostMapping("/calcDeliveryPath") - public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispathRecDto[] record) { - return new DeliveryPathDto(0.0f, 0, new DronePathDto[] {}); + public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispatchRecDto[] record) { + return new DeliveryPathDto(0.0f, 0, new DronePathDto[]{}); } @PostMapping("/calcDeliveryPathAsGeoJson") - public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispathRecDto[] record) { + public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecDto[] record) { return "{}"; } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/AvailabilityTimeSegDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/AvailabilityTimeSegDto.java new file mode 100644 index 0000000..d080bc1 --- /dev/null +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/AvailabilityTimeSegDto.java @@ -0,0 +1,10 @@ +package io.github.js0ny.ilp_coursework.data; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +public record AvailabilityTimeSegDto( + DayOfWeek dayOfWeek, + LocalTime from, + LocalTime until) { +} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/DroneAvailabilityDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/DroneAvailabilityDto.java new file mode 100644 index 0000000..9e2a8f1 --- /dev/null +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/DroneAvailabilityDto.java @@ -0,0 +1,22 @@ +package io.github.js0ny.ilp_coursework.data; + +import java.time.DayOfWeek; +import java.time.LocalTime; + +public record DroneAvailabilityDto( + String id, + AvailabilityTimeSegDto[] availability) { + + public boolean checkAvailability(DayOfWeek day, LocalTime time) { + + for (var a : availability) { + if (a.dayOfWeek().equals(day)) { + if (!time.isBefore(a.from()) && !time.isAfter(a.until())) { + return true; + } + } + } + + return false; + } +} diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/MedDispathRecDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/MedDispatchRecDto.java similarity index 62% rename from src/main/java/io/github/js0ny/ilp_coursework/data/MedDispathRecDto.java rename to src/main/java/io/github/js0ny/ilp_coursework/data/MedDispatchRecDto.java index a1a8857..52e7c4d 100644 --- a/src/main/java/io/github/js0ny/ilp_coursework/data/MedDispathRecDto.java +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/MedDispatchRecDto.java @@ -3,9 +3,10 @@ package io.github.js0ny.ilp_coursework.data; import java.time.LocalDate; import java.time.LocalTime; -public record MedDispathRecDto( +public record MedDispatchRecDto( int id, LocalDate date, LocalTime time, - MedRequirementDto requirements) { + MedRequirementDto requirements, + LngLatDto delivery) { } diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/ServicePointDronesDto.java b/src/main/java/io/github/js0ny/ilp_coursework/data/ServicePointDronesDto.java new file mode 100644 index 0000000..6022969 --- /dev/null +++ b/src/main/java/io/github/js0ny/ilp_coursework/data/ServicePointDronesDto.java @@ -0,0 +1,18 @@ +package io.github.js0ny.ilp_coursework.data; + +import org.springframework.lang.Nullable; + +public record ServicePointDronesDto( + String servicePointId, + DroneAvailabilityDto[] drones) { + + @Nullable + public DroneAvailabilityDto locateDroneById(String droneId) { + for (var drone : drones) { + if (drone.id().equals(droneId)) { + return drone; + } + } + return null; + } +} 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 7acb2c7..fcea5a8 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,12 +2,16 @@ package io.github.js0ny.ilp_coursework.service; import io.github.js0ny.ilp_coursework.data.AttrComparatorDto; import io.github.js0ny.ilp_coursework.data.DroneDto; -import io.github.js0ny.ilp_coursework.data.MedDispathRecDto; +import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto; +import io.github.js0ny.ilp_coursework.data.ServicePointDronesDto; import io.github.js0ny.ilp_coursework.util.AttrOperator; import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched; import java.net.URI; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -23,6 +27,7 @@ public class DroneInfoService { private final String baseUrl; private final String dronesEndpoint = "drones"; + private final String dronesForServicePointsEndpoint = "drones-for-service-points"; private final RestTemplate restTemplate = new RestTemplate(); @@ -49,7 +54,7 @@ public class DroneInfoService { * * @param state determines the capability filtering * @return if {@code state} is true, return ids of drones with cooling - * capability, else without cooling + * capability, else without cooling * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) */ public String[] dronesWithCooling(boolean state) { @@ -59,7 +64,7 @@ public class DroneInfoService { DroneDto[].class); if (drones == null) { - return new String[] {}; + return new String[]{}; } return Arrays.stream(drones) @@ -141,7 +146,7 @@ public class DroneInfoService { } } if (matchingDroneIds == null) { - return new String[] {}; + return new String[]{}; } return matchingDroneIds.toArray(String[]::new); } @@ -157,10 +162,10 @@ public class DroneInfoService { * @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}) + * {@code op}) * @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, - * String, - * AttrOperator) + * String, + * AttrOperator) */ private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) { URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); @@ -170,7 +175,7 @@ public class DroneInfoService { DroneDto[].class); if (drones == null) { - return new String[] {}; + return new String[]{}; } // Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic @@ -192,7 +197,112 @@ public class DroneInfoService { .toArray(String[]::new); } - public String[] dronesMatchesRequirements(MedDispathRecDto[] rec) { - return new String[] {}; + /** + * 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 array of drone ids that match all the requirements + * @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones + */ + public String[] dronesMatchesRequirements(MedDispatchRecDto[] rec) { + URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); + DroneDto[] drones = restTemplate.getForObject( + droneUrl, + DroneDto[].class); + + if (drones == null || rec == null || rec.length == 0) { + return new String[]{}; + } + + /* + * Traverse and filter drones, pass every record's requirement to helper + */ + return Arrays.stream(drones) + .filter(drone -> drone != null && drone.capability() != null) + .filter(drone -> Arrays.stream(rec) + .filter(record -> record != null && record.requirements() != null) + // Every record must be met + .allMatch(record -> meetsRequirement(drone, record))) + .map(DroneDto::id) + .toArray(String[]::new); + } + + /** + * Helper to check if a drone meets the requirement of a medical dispatch. + * + * @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}) + */ + private boolean meetsRequirement(DroneDto drone, MedDispatchRecDto 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"); + } + + float requiredCapacity = requirements.capacity(); + if (requiredCapacity <= 0 || capability.capacity() < requiredCapacity) { + return false; + } + + // Use boolean wrapper to allow null (not specified) values + Boolean requiredCooling = requirements.cooling(); + Boolean requiredHeating = requirements.heating(); + Float requiredMaxCost = requirements.maxCost(); + + boolean matchesCooling = requiredCooling == null || capability.cooling() == requiredCooling; + boolean matchesHeating = requiredHeating == null || capability.heating() == requiredHeating; + boolean matchesCost = false; + + float totalCost = capability.costInitial() + capability.costFinal(); + + if (capability.maxMoves() > 0) { + totalCost += capability.costPerMove(); + } + matchesCost = totalCost <= requiredMaxCost; + + // Conditions: All requirements matched + availability matched, use helper + // For minimal privilege, only pass drone id to check availability + return matchesCooling && matchesHeating && matchesCost && checkAvailability(drone.id(), record); + } + + /** + * 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 + * @return true if the drone is available, false otherwise + */ + private boolean checkAvailability(String droneId, MedDispatchRecDto record) { + URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); + ServicePointDronesDto[] servicePoints = restTemplate.getForObject( + droneUrl, + ServicePointDronesDto[].class); + + LocalDate requiredDate = record.date(); + DayOfWeek requiredDay = requiredDate.getDayOfWeek(); + LocalTime requiredTime = record.time(); + + for (var servicePoint : servicePoints) { + var drone = servicePoint.locateDroneById(droneId); // Nullable + if (drone != null) { + return drone.checkAvailability(requiredDay, requiredTime); + } + + } + + return false; + } }