feat(endpoints): queryAvailableDrones

This commit is contained in:
js0ny 2025-11-24 00:09:19 +00:00
parent 9fbc2f3a14
commit ec0d9087dd
10 changed files with 291 additions and 18 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,8 @@
meta {
name: [POST] queryAvailableDrones
seq: 5
}
auth {
mode: inherit
}

View file

@ -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.DeliveryPathDto;
import io.github.js0ny.ilp_coursework.data.DroneDto; import io.github.js0ny.ilp_coursework.data.DroneDto;
import io.github.js0ny.ilp_coursework.data.DronePathDto; 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 io.github.js0ny.ilp_coursework.service.DroneInfoService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -89,17 +89,17 @@ public class DroneController {
} }
@PostMapping("/queryAvailableDrones") @PostMapping("/queryAvailableDrones")
public String[] queryAvailableDrones(@RequestBody MedDispathRecDto[] records) { public String[] queryAvailableDrones(@RequestBody MedDispatchRecDto[] records) {
return droneService.dronesMatchesRequirements(records); return droneService.dronesMatchesRequirements(records);
} }
@PostMapping("/calcDeliveryPath") @PostMapping("/calcDeliveryPath")
public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispathRecDto[] record) { public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispatchRecDto[] record) {
return new DeliveryPathDto(0.0f, 0, new DronePathDto[]{}); return new DeliveryPathDto(0.0f, 0, new DronePathDto[]{});
} }
@PostMapping("/calcDeliveryPathAsGeoJson") @PostMapping("/calcDeliveryPathAsGeoJson")
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispathRecDto[] record) { public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecDto[] record) {
return "{}"; return "{}";
} }

View file

@ -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) {
}

View file

@ -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;
}
}

View file

@ -3,9 +3,10 @@ package io.github.js0ny.ilp_coursework.data;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
public record MedDispathRecDto( public record MedDispatchRecDto(
int id, int id,
LocalDate date, LocalDate date,
LocalTime time, LocalTime time,
MedRequirementDto requirements) { MedRequirementDto requirements,
LngLatDto delivery) {
} }

View file

@ -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;
}
}

View file

@ -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.AttrComparatorDto;
import io.github.js0ny.ilp_coursework.data.DroneDto; 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 io.github.js0ny.ilp_coursework.util.AttrOperator;
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched; import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
import java.net.URI; import java.net.URI;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -23,6 +27,7 @@ public class DroneInfoService {
private final String baseUrl; private final String baseUrl;
private final String dronesEndpoint = "drones"; private final String dronesEndpoint = "drones";
private final String dronesForServicePointsEndpoint = "drones-for-service-points";
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
@ -192,7 +197,112 @@ public class DroneInfoService {
.toArray(String[]::new); .toArray(String[]::new);
} }
public String[] dronesMatchesRequirements(MedDispathRecDto[] rec) { /**
* Return an array of ids of drones that match all the requirements in the
* medical dispatch records
* <p>
* 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[]{}; 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;
}
} }