chore(format): format to google format
This commit is contained in:
parent
d6e426d1e3
commit
449c81a375
35 changed files with 929 additions and 1138 deletions
39
ilp-cw-api/[POST] queryAvailableDrones/Complex copy copy.bru
Normal file
39
ilp-cw-api/[POST] queryAvailableDrones/Complex copy copy.bru
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
39
ilp-cw-api/[POST] queryAvailableDrones/Complex copy.bru
Normal file
39
ilp-cw-api/[POST] queryAvailableDrones/Complex copy.bru
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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.MovementRequest;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
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.
|
* Main REST Controller for the ILP Coursework 1 application.
|
||||||
* <p>
|
*
|
||||||
* This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in CW1)
|
* <p>This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in
|
||||||
* This is responsible for mapping requests to the appropriate service method and returning the results as responses.
|
* CW1) This is responsible for mapping requests to the appropriate service method and returning the
|
||||||
* The business logic is delegated to {@link GpsCalculationService}
|
* results as responses. The business logic is delegated to {@link GpsCalculationService}
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
|
|
@ -27,9 +28,11 @@ public class ApiController {
|
||||||
private final GpsCalculationService gpsService;
|
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) {
|
public ApiController(GpsCalculationService gpsService) {
|
||||||
this.gpsService = 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
|
* Handles POST requests to check if the two coordinates are close to each other
|
||||||
*
|
*
|
||||||
* @param request A {@link DistanceRequest} containing the two coordinates
|
* @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")
|
@PostMapping("/isCloseTo")
|
||||||
public boolean getIsCloseTo(@RequestBody DistanceRequest request) {
|
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
|
* 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
|
* @return A {@link LngLat} representing the destination
|
||||||
*/
|
*/
|
||||||
@PostMapping("/nextPosition")
|
@PostMapping("/nextPosition")
|
||||||
|
|
|
||||||
|
|
@ -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.DroneAttrComparatorService;
|
||||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||||
import io.github.js0ny.ilp_coursework.service.PathFinderService;
|
import io.github.js0ny.ilp_coursework.service.PathFinderService;
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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.
|
* Main Rest Controller for the ILP Coursework 2 application.
|
||||||
* <p>
|
*
|
||||||
* This class handles incoming HTTP requests for the API under {@code /api/v1}
|
* <p>This class handles incoming HTTP requests for the API under {@code /api/v1} path (defined in
|
||||||
* path (defined in CW2)
|
* CW2) The business logic is delegated to {@link DroneInfoService}
|
||||||
* The business logic is delegated to {@link DroneInfoService}
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
|
|
@ -28,37 +28,33 @@ public class DroneController {
|
||||||
private final PathFinderService pathFinderService;
|
private final PathFinderService pathFinderService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of the {@code DroneController} with the business logic dependency
|
* Constructor of the {@code DroneController} with the business logic dependency {@code
|
||||||
* {@code DroneInfoService}
|
* DroneInfoService}
|
||||||
* <p>
|
*
|
||||||
* We handle the {@code baseUrl} here. Use a predefined URL if the environment
|
* <p>We handle the {@code baseUrl} here. Use a predefined URL if the environment variable
|
||||||
* variable {@code ILP_ENDPOINT}
|
* {@code ILP_ENDPOINT} is not given.
|
||||||
* is not given.
|
|
||||||
*
|
*
|
||||||
* @param droneService The service component that contains all business logic
|
* @param droneService The service component that contains all business logic
|
||||||
*/
|
*/
|
||||||
public DroneController(
|
public DroneController(
|
||||||
DroneInfoService droneService,
|
DroneInfoService droneService,
|
||||||
DroneAttrComparatorService droneAttrComparatorService,
|
DroneAttrComparatorService droneAttrComparatorService,
|
||||||
PathFinderService pathFinderService
|
PathFinderService pathFinderService) {
|
||||||
) {
|
|
||||||
this.droneInfoService = droneService;
|
this.droneInfoService = droneService;
|
||||||
this.droneAttrComparatorService = droneAttrComparatorService;
|
this.droneAttrComparatorService = droneAttrComparatorService;
|
||||||
this.pathFinderService = pathFinderService;
|
this.pathFinderService = pathFinderService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles GET requests to retrieve an array of drones (identified by id) that
|
* Handles GET requests to retrieve an array of drones (identified by id) that has the
|
||||||
* has the capability of cooling
|
* capability of cooling
|
||||||
*
|
*
|
||||||
* @param state The path variable that indicates the return should have or not
|
* @param state The path variable that indicates the return should have or not have the
|
||||||
* have the capability
|
* capability
|
||||||
* @return An array of drone id with cooling capability.
|
* @return An array of drone id with cooling capability.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/dronesWithCooling/{state}")
|
@GetMapping("/dronesWithCooling/{state}")
|
||||||
public List<String> getDronesWithCoolingCapability(
|
public List<String> getDronesWithCoolingCapability(@PathVariable boolean state) {
|
||||||
@PathVariable boolean state
|
|
||||||
) {
|
|
||||||
return droneInfoService.dronesWithCooling(state);
|
return droneInfoService.dronesWithCooling(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,8 +62,8 @@ public class DroneController {
|
||||||
* Handles GET requests to retrieve the drone detail identified by id
|
* Handles GET requests to retrieve the drone detail identified by id
|
||||||
*
|
*
|
||||||
* @param id The id of the drone to be queried.
|
* @param id The id of the drone to be queried.
|
||||||
* @return 200 with {@link Drone}-style json if success, 404 if {@code id}
|
* @return 200 with {@link Drone}-style json if success, 404 if {@code id} not found, 400
|
||||||
* not found, 400 otherwise
|
* otherwise
|
||||||
*/
|
*/
|
||||||
@GetMapping("/droneDetails/{id}")
|
@GetMapping("/droneDetails/{id}")
|
||||||
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
|
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
|
||||||
|
|
@ -80,51 +76,36 @@ public class DroneController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles GET requests to retrieve an array of drone ids that
|
* Handles GET requests to retrieve an array of drone ids that {@code capability.attrName =
|
||||||
* {@code capability.attrName = attrVal}
|
* attrVal}
|
||||||
*
|
*
|
||||||
* @param attrName The name of the attribute to be queried
|
* @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
|
* @return An array of drone id that matches the attribute name and value
|
||||||
*/
|
*/
|
||||||
@GetMapping("/queryAsPath/{attrName}/{attrVal}")
|
@GetMapping("/queryAsPath/{attrName}/{attrVal}")
|
||||||
public List<String> getIdByAttrMap(
|
public List<String> getIdByAttrMap(
|
||||||
@PathVariable String attrName,
|
@PathVariable String attrName, @PathVariable String attrVal) {
|
||||||
@PathVariable String attrVal
|
return droneAttrComparatorService.dronesWithAttribute(attrName, attrVal);
|
||||||
) {
|
|
||||||
return droneAttrComparatorService.dronesWithAttribute(
|
|
||||||
attrName,
|
|
||||||
attrVal
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
public List<String> getIdByAttrMapPost(
|
public List<String> getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
|
||||||
@RequestBody AttrQueryRequest[] attrComparators
|
return droneAttrComparatorService.dronesSatisfyingAttributes(attrComparators);
|
||||||
) {
|
|
||||||
return droneAttrComparatorService.dronesSatisfyingAttributes(
|
|
||||||
attrComparators
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/queryAvailableDrones")
|
@PostMapping("/queryAvailableDrones")
|
||||||
public List<String> queryAvailableDrones(
|
public List<String> queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
|
||||||
@RequestBody MedDispatchRecRequest[] records
|
|
||||||
) {
|
|
||||||
return droneInfoService.dronesMatchesRequirements(records);
|
return droneInfoService.dronesMatchesRequirements(records);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/calcDeliveryPath")
|
@PostMapping("/calcDeliveryPath")
|
||||||
public DeliveryPathResponse calculateDeliveryPath(
|
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
|
||||||
@RequestBody MedDispatchRecRequest[] record
|
|
||||||
) {
|
|
||||||
return pathFinderService.calculateDeliveryPath(record);
|
return pathFinderService.calculateDeliveryPath(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/calcDeliveryPathAsGeoJson")
|
@PostMapping("/calcDeliveryPathAsGeoJson")
|
||||||
public String calculateDeliveryPathAsGeoJson(
|
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
|
||||||
@RequestBody MedDispatchRecRequest[] record
|
|
||||||
) {
|
|
||||||
return pathFinderService.calculateDeliveryPathAsGeoJson(record);
|
return pathFinderService.calculateDeliveryPathAsGeoJson(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
public class GeoJsonDataController {
|
public class GeoJsonDataController {
|
||||||
|
|
|
||||||
|
|
@ -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}).
|
* Represents a range of altitude values (that is a fly-zone in {@link RestrictedArea}).
|
||||||
*
|
*
|
||||||
* @param lower The lower bound of the altitude range.
|
* @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
|
* @param upper The upper bound of the altitude range. If {@code upper = -1}, then the region is not
|
||||||
* is not a fly zone.
|
* a fly zone.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public record AltitudeRange(double lower, double upper) {
|
public record AltitudeRange(double lower, double upper) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ public record Angle(double degrees) {
|
||||||
|
|
||||||
public Angle {
|
public Angle {
|
||||||
if (degrees < 0 || degrees >= 360) {
|
if (degrees < 0 || degrees >= 360) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("Angle must be in range [0, 360). Got: " + degrees);
|
||||||
"Angle must be in range [0, 360). Got: " + degrees);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be a multiple of 22.5 (one of the 16 major directions)
|
// 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
|
// 1.0e-15
|
||||||
// So we need to check if the remainder is small enough, or close enough to STEP
|
// So we need to check if the remainder is small enough, or close enough to STEP
|
||||||
// (handling negative errors)
|
// (handling negative errors)
|
||||||
if (Math.abs(remainder) > EPSILON &&
|
if (Math.abs(remainder) > EPSILON && Math.abs(remainder - STEP) > EPSILON) {
|
||||||
Math.abs(remainder - STEP) > EPSILON) {
|
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Angle must be a multiple of 22.5 (one of the 16 major directions). Got: " +
|
"Angle must be a multiple of 22.5 (one of the 16 major directions). Got: "
|
||||||
degrees);
|
+ degrees);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Angle fromIndex(int index) {
|
public static Angle fromIndex(int index) {
|
||||||
if (index < 0 || index > 15) {
|
if (index < 0 || index > 15) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("Direction index must be between 0 and 15");
|
||||||
"Direction index must be between 0 and 15");
|
|
||||||
}
|
}
|
||||||
return new Angle(index * STEP);
|
return new Angle(index * STEP);
|
||||||
}
|
}
|
||||||
|
|
@ -64,5 +61,4 @@ public record Angle(double degrees) {
|
||||||
public double toRadians() {
|
public double toRadians() {
|
||||||
return Math.toRadians(degrees);
|
return Math.toRadians(degrees);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ package io.github.js0ny.ilp_coursework.data.common;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
|
|
||||||
public record DroneAvailability(
|
public record DroneAvailability(String id, TimeWindow[] availability) {
|
||||||
String id,
|
|
||||||
TimeWindow[] availability) {
|
|
||||||
|
|
||||||
public boolean checkAvailability(DayOfWeek day, LocalTime time) {
|
public boolean checkAvailability(DayOfWeek day, LocalTime time) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,4 @@ public record DroneCapability(
|
||||||
int maxMoves,
|
int maxMoves,
|
||||||
float costPerMove,
|
float costPerMove,
|
||||||
float costInitial,
|
float costInitial,
|
||||||
float costFinal) {
|
float costFinal) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a point or coordinate
|
* Represents the data transfer object for a point or coordinate that defines by a longitude and
|
||||||
* that defines by a longitude and latitude
|
* latitude
|
||||||
*
|
*
|
||||||
* @param lng longitude of the coordinate/point
|
* @param lng longitude of the coordinate/point
|
||||||
* @param lat latitude of the coordinate/point
|
* @param lat latitude of the coordinate/point
|
||||||
|
|
@ -13,14 +13,12 @@ public record LngLat(double lng, double lat) {
|
||||||
public LngLat {
|
public LngLat {
|
||||||
if (lat < -90 || lat > 90) {
|
if (lat < -90 || lat > 90) {
|
||||||
throw new IllegalArgumentException(
|
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) {
|
if (lng < -180 || lng > 180) {
|
||||||
throw new IllegalArgumentException(
|
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) {
|
if (other == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return (Math.abs(lng - other.lng()) < EPSILON && Math.abs(lat - other.lat()) < EPSILON);
|
||||||
Math.abs(lng - other.lng()) < EPSILON &&
|
|
||||||
Math.abs(lat - other.lat()) < EPSILON
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a point or coordinate
|
* Represents the data transfer object for a point or coordinate that defines by a longitude and
|
||||||
* that defines by a longitude and latitude
|
* latitude
|
||||||
*
|
*
|
||||||
* @param lng longitude of the coordinate/point
|
* @param lng longitude of the coordinate/point
|
||||||
* @param lat latitude of the coordinate/point
|
* @param lat latitude of the coordinate/point
|
||||||
* @param alt altitude 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) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,42 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a region definition
|
* Represents the data transfer object for a region definition
|
||||||
* <p>
|
|
||||||
* This record encapsulates the data for calculating if a coordinate is inside
|
|
||||||
* the region
|
|
||||||
* <p>
|
|
||||||
* 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
|
* <p>This record encapsulates the data for calculating if a coordinate is inside the region
|
||||||
|
*
|
||||||
|
* <p>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.
|
* @param vertices list of coordinates that forms a polygon as a region.
|
||||||
* <p>
|
* <p>In order to define a valid region, the last element of the list should be the same as the
|
||||||
* In order to define a valid region, the last element of the
|
* first, or known as closed
|
||||||
* list should be the same as the first, or
|
|
||||||
* known as closed
|
|
||||||
* @see RegionCheckRequest
|
* @see RegionCheckRequest
|
||||||
* @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat,
|
* @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat, Region)
|
||||||
* Region)
|
|
||||||
*/
|
*/
|
||||||
public record Region(String name, List<LngLat> vertices) {
|
public record Region(String name, List<LngLat> vertices) {
|
||||||
/**
|
/**
|
||||||
* Magic number 4: For a polygon, 3 edges is required.
|
* Magic number 4: For a polygon, 3 edges is required.
|
||||||
* <p>
|
*
|
||||||
* In this dto, edges + 1 vertices is required.
|
* <p>In this dto, edges + 1 vertices is required.
|
||||||
*/
|
*/
|
||||||
private static final int MINIMUM_VERTICES = 4;
|
private static final int MINIMUM_VERTICES = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to check if the region has a valid polygon by checking if the
|
* Method to check if the region has a valid polygon by checking if the {@code vertices} forms a
|
||||||
* {@code vertices} forms a closed polygon
|
* closed polygon
|
||||||
*
|
*
|
||||||
* @return {@code true} if the {@code vertices} are able to form a polygon and
|
* @return {@code true} if the {@code vertices} are able to form a polygon and form a closed
|
||||||
* form a closed polygon
|
* polygon
|
||||||
*/
|
*/
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
if (vertices == null || vertices.size() < MINIMUM_VERTICES) {
|
if (vertices == null || vertices.size() < MINIMUM_VERTICES) {
|
||||||
|
|
@ -56,13 +51,10 @@ public record Region(String name, List<LngLat> vertices) {
|
||||||
try {
|
try {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
List<List<Double>> ring = vertices.stream()
|
List<List<Double>> ring =
|
||||||
.map(v -> List.of(v.lng(), v.lat()))
|
vertices.stream().map(v -> List.of(v.lng(), v.lat())).toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Map.of(
|
return Map.of("type", "Polygon", "coordinates", List.of(ring));
|
||||||
"type", "Polygon",
|
|
||||||
"coordinates", List.of(ring));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to generate GeoJSON", e);
|
throw new RuntimeException("Failed to generate GeoJSON", e);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,4 @@ package io.github.js0ny.ilp_coursework.data.common;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
|
|
||||||
public record TimeWindow(
|
public record TimeWindow(DayOfWeek dayOfWeek, LocalTime from, LocalTime until) {}
|
||||||
DayOfWeek dayOfWeek,
|
|
||||||
LocalTime from,
|
|
||||||
LocalTime until) {
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,8 @@ package io.github.js0ny.ilp_coursework.data.external;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
|
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
|
||||||
|
|
||||||
/**
|
/** Represents the data transfer object for a drone, gained from the endpoints */
|
||||||
* Represents the data transfer object for a drone, gained from the endpoints
|
public record Drone(String name, String id, DroneCapability capability) {
|
||||||
*/
|
|
||||||
public record Drone(
|
|
||||||
String name,
|
|
||||||
String id,
|
|
||||||
DroneCapability capability) {
|
|
||||||
|
|
||||||
public int parseId() {
|
public int parseId() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -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.LngLat;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record RestrictedArea(
|
public record RestrictedArea(String name, int id, AltitudeRange limits, LngLatAlt[] vertices) {
|
||||||
String name,
|
|
||||||
int id,
|
|
||||||
AltitudeRange limits,
|
|
||||||
LngLatAlt[] vertices
|
|
||||||
) {
|
|
||||||
public Region toRegion() {
|
public Region toRegion() {
|
||||||
List<LngLat> vertices2D = new ArrayList<>();
|
List<LngLat> vertices2D = new ArrayList<>();
|
||||||
for (var vertex : vertices) {
|
for (var vertex : vertices) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.external;
|
package io.github.js0ny.ilp_coursework.data.external;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
|
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
|
||||||
|
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
public record ServicePointDrones(
|
public record ServicePointDrones(int servicePointId, DroneAvailability[] drones) {
|
||||||
int servicePointId,
|
|
||||||
DroneAvailability[] drones) {
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public DroneAvailability locateDroneById(String droneId) {
|
public DroneAvailability locateDroneById(String droneId) {
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,4 @@ package io.github.js0ny.ilp_coursework.data.request;
|
||||||
// TODO: Convert operator to Enum
|
// TODO: Convert operator to Enum
|
||||||
// import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
// 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) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,11 @@ import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a distance operation request.
|
* Represents the data transfer object for a distance operation request.
|
||||||
* <p>
|
*
|
||||||
* This record encapsulates the data for several endpoints that involves two {@code LngLatDto}
|
* <p>This record encapsulates the data for several endpoints that involves two {@code LngLatDto}
|
||||||
* and serves as the data contract for those API operation
|
* and serves as the data contract for those API operation
|
||||||
*
|
*
|
||||||
* @param position1 Nested object of {@link LngLat}
|
* @param position1 Nested object of {@link LngLat}
|
||||||
* @param position2 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) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.request;
|
package io.github.js0ny.ilp_coursework.data.request;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
@ -8,17 +9,7 @@ import java.time.LocalTime;
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record MedDispatchRecRequest(
|
public record MedDispatchRecRequest(
|
||||||
int id,
|
int id, LocalDate date, LocalTime time, MedRequirement requirements, LngLat delivery) {
|
||||||
LocalDate date,
|
|
||||||
LocalTime time,
|
|
||||||
MedRequirement requirements,
|
|
||||||
LngLat delivery) {
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record MedRequirement(
|
public record MedRequirement(float capacity, boolean cooling, boolean heating, float maxCost) {}
|
||||||
float capacity,
|
|
||||||
boolean cooling,
|
|
||||||
boolean heating,
|
|
||||||
float maxCost
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@ import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a movement action request.
|
* Represents the data transfer object for a movement action request.
|
||||||
* <p>
|
*
|
||||||
* This record encapsulates the data for endpoint /api/v1/nextPosition and serves as the data contract for
|
* <p>This record encapsulates the data for endpoint /api/v1/nextPosition and serves as the data
|
||||||
* this API operation
|
* contract for this API operation
|
||||||
*
|
*
|
||||||
* @param start The starting coordinate of the movement
|
* @param start The starting coordinate of the movement
|
||||||
* @param angle The angle to movement in degree. This corresponds to compass directions.
|
* @param angle The angle to movement in degree. This corresponds to compass directions. For
|
||||||
* For example: 0 for East, 90 for North
|
* example: 0 for East, 90 for North
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
||||||
*/
|
*/
|
||||||
public record MovementRequest(LngLat start, double angle) {
|
public record MovementRequest(LngLat start, double angle) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,14 @@ import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a region check request.
|
* Represents the data transfer object for a region check request.
|
||||||
* <p>
|
*
|
||||||
* This record encapsulates the data for endpoint /api/v1/isInRegion and serves as the data contract for
|
* <p>This record encapsulates the data for endpoint /api/v1/isInRegion and serves as the data
|
||||||
* this API operation
|
* contract for this API operation
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
*
|
*
|
||||||
* @param position The coordinate to be checked
|
* @param position The coordinate to be checked
|
||||||
* @param region The region for the check.
|
* @param region The region for the check. This is a nested object represented by {@link Region}
|
||||||
* This is a nested object represented by {@link Region}
|
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
|
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
|
||||||
*/
|
*/
|
||||||
public record RegionCheckRequest(LngLat position, Region region) {
|
public record RegionCheckRequest(LngLat position, Region region) {}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.response;
|
package io.github.js0ny.ilp_coursework.data.response;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record DeliveryPathResponse(
|
public record DeliveryPathResponse(float totalCost, int totalMoves, DronePath[] dronePaths) {
|
||||||
float totalCost,
|
|
||||||
int totalMoves,
|
|
||||||
DronePath[] dronePaths
|
|
||||||
) {
|
|
||||||
public record DronePath(int droneId, List<Delivery> deliveries) {
|
public record DronePath(int droneId, List<Delivery> deliveries) {
|
||||||
public record Delivery(int deliveryId, List<LngLat> flightPath) {}
|
public record Delivery(int deliveryId, List<LngLat> flightPath) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
package io.github.js0ny.ilp_coursework.exception;
|
package io.github.js0ny.ilp_coursework.exception;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
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.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
/**
|
import java.util.Map;
|
||||||
* Class that handles exception or failed request. Map all error requests to 400.
|
import java.util.Optional;
|
||||||
*/
|
|
||||||
|
/** Class that handles exception or failed request. Map all error requests to 400. */
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
/// Use a logger to save logs instead of passing them to user
|
/// Use a logger to save logs instead of passing them to user
|
||||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
private final Map<String, String> badRequestMap = Map.of("status", "400", "error", "Bad Request");
|
private final Map<String, String> badRequestMap =
|
||||||
|
Map.of("status", "400", "error", "Bad Request");
|
||||||
|
|
||||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
|
@ -31,8 +30,8 @@ public class GlobalExceptionHandler {
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
|
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
String errorMessage = Optional.ofNullable(ex.getMessage())
|
String errorMessage =
|
||||||
.orElse("Invalid argument provided.");
|
Optional.ofNullable(ex.getMessage()).orElse("Invalid argument provided.");
|
||||||
log.warn("Illegal argument in request: {}", errorMessage);
|
log.warn("Illegal argument in request: {}", errorMessage);
|
||||||
return badRequestMap;
|
return badRequestMap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
||||||
import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
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.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -14,8 +19,6 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DroneAttrComparatorService {
|
public class DroneAttrComparatorService {
|
||||||
|
|
@ -24,14 +27,11 @@ public class DroneAttrComparatorService {
|
||||||
private final String dronesEndpoint = "drones";
|
private final String dronesEndpoint = "drones";
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
/**
|
/** Constructor, handles the base url here. */
|
||||||
* Constructor, handles the base url here.
|
|
||||||
*/
|
|
||||||
public DroneAttrComparatorService() {
|
public DroneAttrComparatorService() {
|
||||||
String baseUrl = System.getenv("ILP_ENDPOINT");
|
String baseUrl = System.getenv("ILP_ENDPOINT");
|
||||||
if (baseUrl == null || baseUrl.isBlank()) {
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
this.baseUrl =
|
this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
||||||
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
|
||||||
} else {
|
} else {
|
||||||
// Defensive: Add '/' to the end of the URL
|
// Defensive: Add '/' to the end of the URL
|
||||||
if (!baseUrl.endsWith("/")) {
|
if (!baseUrl.endsWith("/")) {
|
||||||
|
|
@ -43,11 +43,11 @@ public class DroneAttrComparatorService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones with a given attribute name and value.
|
* Return an array of ids of drones with a given attribute name and value.
|
||||||
* <p>
|
*
|
||||||
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
|
* <p>Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
|
||||||
*
|
*
|
||||||
* @param attrName the attribute name to filter on
|
* @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
|
* @return array of drone ids matching the attribute name and value
|
||||||
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
|
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
|
* @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
|
* Return an array of ids of drones which matches all given complex comparing rules
|
||||||
* rules
|
|
||||||
*
|
*
|
||||||
* @param attrComparators The filter rule with Name, Value and Operator
|
* @param attrComparators The filter rule with Name, Value and Operator
|
||||||
* @return array of drone ids that matches all rules
|
* @return array of drone ids that matches all rules
|
||||||
*/
|
*/
|
||||||
public List<String> dronesSatisfyingAttributes(
|
public List<String> dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
|
||||||
AttrQueryRequest[] attrComparators
|
|
||||||
) {
|
|
||||||
Set<String> matchingDroneIds = null;
|
Set<String> matchingDroneIds = null;
|
||||||
for (var comparator : attrComparators) {
|
for (var comparator : attrComparators) {
|
||||||
String attribute = comparator.attribute();
|
String attribute = comparator.attribute();
|
||||||
String operator = comparator.operator();
|
String operator = comparator.operator();
|
||||||
String value = comparator.value();
|
String value = comparator.value();
|
||||||
AttrOperator op = AttrOperator.fromString(operator);
|
AttrOperator op = AttrOperator.fromString(operator);
|
||||||
List<String> ids = dronesWithAttributeCompared(
|
List<String> ids = dronesWithAttributeCompared(attribute, value, op);
|
||||||
attribute,
|
|
||||||
value,
|
|
||||||
op
|
|
||||||
);
|
|
||||||
if (matchingDroneIds == null) {
|
if (matchingDroneIds == null) {
|
||||||
matchingDroneIds = new HashSet<>(ids);
|
matchingDroneIds = new HashSet<>(ids);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -92,25 +85,20 @@ public class DroneAttrComparatorService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper that wraps the dynamic querying with different comparison operators
|
* Helper that wraps the dynamic querying with different comparison operators
|
||||||
* <p>
|
*
|
||||||
* This method act as a concatenation of
|
* <p>This method act as a concatenation of {@link
|
||||||
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
* io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
||||||
* AttrOperator)}
|
* AttrOperator)}
|
||||||
*
|
*
|
||||||
* @param attrName the attribute name to filter on
|
* @param attrName the attribute name to filter on
|
||||||
* @param attrVal the attribute value to filter on
|
* @param attrVal the attribute value to filter on
|
||||||
* @param op the comparison operator
|
* @param op the comparison operator
|
||||||
* @return array of drone ids matching the attribute name and value (filtered by
|
* @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,
|
||||||
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
|
* AttrOperator)
|
||||||
* String,
|
|
||||||
* AttrOperator)
|
|
||||||
*/
|
*/
|
||||||
private List<String> dronesWithAttributeCompared(
|
private List<String> dronesWithAttributeCompared(
|
||||||
String attrName,
|
String attrName, String attrVal, AttrOperator op) {
|
||||||
String attrVal,
|
|
||||||
AttrOperator op
|
|
||||||
) {
|
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
// This is required to make sure the response is valid
|
// This is required to make sure the response is valid
|
||||||
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||||
|
|
@ -124,17 +112,18 @@ public class DroneAttrComparatorService {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
return Arrays.stream(drones)
|
return Arrays.stream(drones)
|
||||||
.filter(drone -> {
|
.filter(
|
||||||
JsonNode node = mapper.valueToTree(drone);
|
drone -> {
|
||||||
JsonNode attrNode = node.findValue(attrName);
|
JsonNode node = mapper.valueToTree(drone);
|
||||||
if (attrNode != null) {
|
JsonNode attrNode = node.findValue(attrName);
|
||||||
// Manually handle different types of JsonNode
|
if (attrNode != null) {
|
||||||
return isValueMatched(attrNode, attrVal, op);
|
// Manually handle different types of JsonNode
|
||||||
} else {
|
return isValueMatched(attrNode, attrVal, op);
|
||||||
return false;
|
} else {
|
||||||
}
|
return false;
|
||||||
})
|
}
|
||||||
.map(Drone::id)
|
})
|
||||||
.collect(Collectors.toList());
|
.map(Drone::id)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
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.common.Region;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
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.ServicePoint;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
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;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DroneInfoService {
|
public class DroneInfoService {
|
||||||
|
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
private final String dronesForServicePointsEndpoint =
|
private final String dronesForServicePointsEndpoint = "drones-for-service-points";
|
||||||
"drones-for-service-points";
|
|
||||||
public static final String servicePointsEndpoint = "service-points";
|
public static final String servicePointsEndpoint = "service-points";
|
||||||
public static final String restrictedAreasEndpoint = "restricted-areas";
|
public static final String restrictedAreasEndpoint = "restricted-areas";
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
/**
|
/** Constructor, handles the base url here. */
|
||||||
* Constructor, handles the base url here.
|
|
||||||
*/
|
|
||||||
public DroneInfoService() {
|
public DroneInfoService() {
|
||||||
this(new RestTemplate());
|
this(new RestTemplate());
|
||||||
}
|
}
|
||||||
|
|
@ -41,8 +41,7 @@ public class DroneInfoService {
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
String baseUrl = System.getenv("ILP_ENDPOINT");
|
String baseUrl = System.getenv("ILP_ENDPOINT");
|
||||||
if (baseUrl == null || baseUrl.isBlank()) {
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
this.baseUrl =
|
this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
||||||
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
|
||||||
} else {
|
} else {
|
||||||
// Defensive: Add '/' to the end of the URL
|
// Defensive: Add '/' to the end of the URL
|
||||||
if (!baseUrl.endsWith("/")) {
|
if (!baseUrl.endsWith("/")) {
|
||||||
|
|
@ -54,13 +53,14 @@ public class DroneInfoService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones with/without cooling capability
|
* Return an array of ids of drones with/without cooling capability
|
||||||
* <p>
|
*
|
||||||
* Associated service method with {@code /dronesWithCooling/{state}}
|
* <p>Associated service method with {@code /dronesWithCooling/{state}}
|
||||||
*
|
*
|
||||||
* @param state determines the capability filtering
|
* @param state determines the capability filtering
|
||||||
* @return if {@code state} is true, return ids of drones with cooling
|
* @return if {@code state} is true, return ids of drones with cooling capability, else without
|
||||||
* capability, else without cooling
|
* cooling
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
* @see
|
||||||
|
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||||
*/
|
*/
|
||||||
public List<String> dronesWithCooling(boolean state) {
|
public List<String> dronesWithCooling(boolean state) {
|
||||||
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
|
|
@ -71,25 +71,22 @@ public class DroneInfoService {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return drones
|
return drones.stream()
|
||||||
.stream()
|
.filter(drone -> drone.capability().cooling() == state)
|
||||||
.filter(drone -> drone.capability().cooling() == state)
|
.map(Drone::id)
|
||||||
.map(Drone::id)
|
.collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a {@link Drone}-style json data structure with the given {@code id}
|
* Return a {@link Drone}-style json data structure with the given {@code id}
|
||||||
* <p>
|
*
|
||||||
* Associated service method with {@code /droneDetails/{id}}
|
* <p>Associated service method with {@code /droneDetails/{id}}
|
||||||
*
|
*
|
||||||
* @param id The id of the drone
|
* @param id The id of the drone
|
||||||
* @return drone json body of given id
|
* @return drone json body of given id
|
||||||
* @throws NullPointerException when cannot fetch available drones from
|
* @throws NullPointerException when cannot fetch available drones from remote
|
||||||
* remote
|
* @throws IllegalArgumentException when drone with given {@code id} cannot be found this should
|
||||||
* @throws IllegalArgumentException when drone with given {@code id} cannot be
|
* lead to a 404
|
||||||
* found
|
|
||||||
* this should lead to a 404
|
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
||||||
*/
|
*/
|
||||||
public Drone droneDetail(String id) {
|
public Drone droneDetail(String id) {
|
||||||
|
|
@ -102,16 +99,14 @@ public class DroneInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will result in 404
|
// This will result in 404
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("drone with that ID cannot be found");
|
||||||
"drone with that ID cannot be found"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones that match all the requirements in the
|
* Return an array of ids of drones that match all the requirements in the medical dispatch
|
||||||
* medical dispatch records
|
* records
|
||||||
* <p>
|
*
|
||||||
* Associated service method with
|
* <p>Associated service method with
|
||||||
*
|
*
|
||||||
* @param rec array of medical dispatch records
|
* @param rec array of medical dispatch records
|
||||||
* @return List of drone ids that match all the requirements
|
* @return List of drone ids that match all the requirements
|
||||||
|
|
@ -121,51 +116,43 @@ public class DroneInfoService {
|
||||||
List<Drone> drones = fetchAllDrones();
|
List<Drone> drones = fetchAllDrones();
|
||||||
|
|
||||||
if (rec == null || rec.length == 0) {
|
if (rec == null || rec.length == 0) {
|
||||||
return drones
|
return drones.stream()
|
||||||
.stream()
|
.filter(Objects::nonNull)
|
||||||
.filter(Objects::nonNull)
|
.map(Drone::id)
|
||||||
.map(Drone::id)
|
.collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Traverse and filter drones, pass every record's requirement to helper
|
* Traverse and filter drones, pass every record's requirement to helper
|
||||||
*/
|
*/
|
||||||
return drones
|
return drones.stream()
|
||||||
.stream()
|
.filter(d -> d != null && d.capability() != null)
|
||||||
.filter(d -> d != null && d.capability() != null)
|
.filter(
|
||||||
.filter(d ->
|
d ->
|
||||||
Arrays.stream(rec)
|
Arrays.stream(rec)
|
||||||
.filter(r -> r != null && r.requirements() != null)
|
.filter(r -> r != null && r.requirements() != null)
|
||||||
.allMatch(r -> droneMatchesRequirement(d, r))
|
.allMatch(r -> droneMatchesRequirement(d, r)))
|
||||||
)
|
.map(Drone::id)
|
||||||
.map(Drone::id)
|
.collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to check if a drone meets the requirement of a medical dispatch.
|
* 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
|
* @param record the medical dispatch record containing the requirement
|
||||||
* @return true if the drone meets the requirement, false otherwise
|
* @return true if the drone meets the requirement, false otherwise
|
||||||
* @throws IllegalArgumentException when record requirements or drone capability
|
* @throws IllegalArgumentException when record requirements or drone capability is invalid
|
||||||
* is invalid (capacity and id cannot be null
|
* (capacity and id cannot be null in {@code MedDispathRecDto})
|
||||||
* in {@code MedDispathRecDto})
|
|
||||||
*/
|
*/
|
||||||
public boolean droneMatchesRequirement(
|
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
||||||
Drone drone,
|
|
||||||
MedDispatchRecRequest record
|
|
||||||
) {
|
|
||||||
var requirements = record.requirements();
|
var requirements = record.requirements();
|
||||||
if (requirements == null) {
|
if (requirements == null) {
|
||||||
throw new IllegalArgumentException("requirements cannot be null");
|
throw new IllegalArgumentException("requirements cannot be null");
|
||||||
}
|
}
|
||||||
var capability = drone.capability();
|
var capability = drone.capability();
|
||||||
if (capability == null) {
|
if (capability == null) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("drone capability cannot be null");
|
||||||
"drone capability cannot be null"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float requiredCapacity = requirements.capacity();
|
float requiredCapacity = requirements.capacity();
|
||||||
|
|
@ -187,11 +174,7 @@ public class DroneInfoService {
|
||||||
|
|
||||||
// Conditions: All requirements matched + availability matched, use helper
|
// Conditions: All requirements matched + availability matched, use helper
|
||||||
// For minimal privilege, only pass drone id to check availability
|
// For minimal privilege, only pass drone id to check availability
|
||||||
return (
|
return (matchesCooling && matchesHeating && checkAvailability(drone.id(), record)); // &&
|
||||||
matchesCooling &&
|
|
||||||
matchesHeating &&
|
|
||||||
checkAvailability(drone.id(), record)
|
|
||||||
); // &&
|
|
||||||
// checkCost(drone, record) // checkCost is more expensive than
|
// checkCost(drone, record) // checkCost is more expensive than
|
||||||
// checkAvailability
|
// checkAvailability
|
||||||
}
|
}
|
||||||
|
|
@ -200,21 +183,13 @@ public class DroneInfoService {
|
||||||
* Helper to check if a drone is available at the required date and time
|
* 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 droneId the id of the drone to be checked
|
||||||
* @param record the medical dispatch record containing the required date and
|
* @param record the medical dispatch record containing the required date and time
|
||||||
* time
|
|
||||||
* @return true if the drone is available, false otherwise
|
* @return true if the drone is available, false otherwise
|
||||||
*/
|
*/
|
||||||
private boolean checkAvailability(
|
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
||||||
String droneId,
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
MedDispatchRecRequest record
|
ServicePointDrones[] servicePoints =
|
||||||
) {
|
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(
|
|
||||||
dronesForServicePointsEndpoint
|
|
||||||
);
|
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
ServicePointDrones[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
LocalDate requiredDate = record.date();
|
LocalDate requiredDate = record.date();
|
||||||
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
||||||
|
|
@ -232,13 +207,9 @@ public class DroneInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
dronesForServicePointsEndpoint
|
ServicePointDrones[] servicePoints =
|
||||||
);
|
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
ServicePointDrones[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -253,14 +224,10 @@ public class DroneInfoService {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private LngLat queryServicePointLocation(int id) {
|
private LngLat queryServicePointLocation(int id) {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
servicePointsEndpoint
|
|
||||||
);
|
|
||||||
|
|
||||||
ServicePoint[] servicePoints = restTemplate.getForObject(
|
ServicePoint[] servicePoints =
|
||||||
servicePointUrl,
|
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
ServicePoint[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -282,49 +249,32 @@ public class DroneInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RestrictedArea> fetchRestrictedAreas() {
|
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||||
URI restrictedUrl = URI.create(baseUrl).resolve(
|
URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
|
||||||
restrictedAreasEndpoint
|
RestrictedArea[] restrictedAreas =
|
||||||
);
|
restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
|
||||||
RestrictedArea[] restrictedAreas = restTemplate.getForObject(
|
|
||||||
restrictedUrl,
|
|
||||||
RestrictedArea[].class
|
|
||||||
);
|
|
||||||
assert restrictedAreas != null;
|
assert restrictedAreas != null;
|
||||||
return Arrays.asList(restrictedAreas);
|
return Arrays.asList(restrictedAreas);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> fetchRestrictedAreasInGeoJson()
|
public List<String> fetchRestrictedAreasInGeoJson() throws JsonProcessingException {
|
||||||
throws JsonProcessingException {
|
|
||||||
var mapper = new ObjectMapper();
|
var mapper = new ObjectMapper();
|
||||||
var ras = fetchRestrictedAreas();
|
var ras = fetchRestrictedAreas();
|
||||||
var geoJson = ras
|
var geoJson = ras.stream().map(RestrictedArea::toRegion).map(Region::toGeoJson).toList();
|
||||||
.stream()
|
|
||||||
.map(RestrictedArea::toRegion)
|
|
||||||
.map(Region::toGeoJson)
|
|
||||||
.toList();
|
|
||||||
return Collections.singletonList(mapper.writeValueAsString(geoJson));
|
return Collections.singletonList(mapper.writeValueAsString(geoJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServicePoint> fetchServicePoints() {
|
public List<ServicePoint> fetchServicePoints() {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
servicePointsEndpoint
|
ServicePoint[] servicePoints =
|
||||||
);
|
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
ServicePoint[] servicePoints = restTemplate.getForObject(
|
|
||||||
servicePointUrl,
|
|
||||||
ServicePoint[].class
|
|
||||||
);
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
return Arrays.asList(servicePoints);
|
return Arrays.asList(servicePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||||
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
|
URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
dronesForServicePointsEndpoint
|
ServicePointDrones[] servicePointDrones =
|
||||||
);
|
restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class);
|
||||||
ServicePointDrones[] servicePointDrones = restTemplate.getForObject(
|
|
||||||
servicePointDronesUrl,
|
|
||||||
ServicePointDrones[].class
|
|
||||||
);
|
|
||||||
assert servicePointDrones != null;
|
assert servicePointDrones != null;
|
||||||
return Arrays.asList(servicePointDrones);
|
return Arrays.asList(servicePointDrones);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.DistanceRequest;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
|
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles calculations about Coordinates
|
* Class that handles calculations about Coordinates
|
||||||
*
|
*
|
||||||
|
|
@ -24,6 +26,7 @@ public class GpsCalculationService {
|
||||||
* @see #nextPosition(LngLat, double)
|
* @see #nextPosition(LngLat, double)
|
||||||
*/
|
*/
|
||||||
private static final double STEP = 0.00015;
|
private static final double STEP = 0.00015;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given threshold to judge if two points are close to each other
|
* 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;
|
private static final double CLOSE_THRESHOLD = 0.00015;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the Euclidean distance between {@code position1} and
|
* Calculate the Euclidean distance between {@code position1} and {@code position2}, which are
|
||||||
* {@code position2}, which are coordinates
|
* coordinates defined as {@link LngLat}
|
||||||
* defined as {@link LngLat}
|
|
||||||
*
|
*
|
||||||
* @param position1 The coordinate of the first position
|
* @param position1 The coordinate of the first position
|
||||||
* @param position2 The coordinate of the second position
|
* @param position2 The coordinate of the second position
|
||||||
* @return The Euclidean distance between {@code position1} and
|
* @return The Euclidean distance between {@code position1} and {@code position2}
|
||||||
* {@code position2}
|
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest)
|
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest)
|
||||||
*/
|
*/
|
||||||
public double calculateDistance(LngLat position1, LngLat position2) {
|
public double calculateDistance(LngLat position1, LngLat position2) {
|
||||||
|
|
@ -55,17 +56,14 @@ public class GpsCalculationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if {@code position1} and
|
* Check if {@code position1} and {@code position2} are close to each other, the threshold is <
|
||||||
* {@code position2} are close to each other, the threshold is < 0.00015
|
* 0.00015
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>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 position1 The coordinate of the first position
|
||||||
* @param position2 The coordinate of the second position
|
* @param position2 The coordinate of the second position
|
||||||
* @return {@code true} if {@code position1} and
|
* @return {@code true} if {@code position1} and {@code position2} are close to each other
|
||||||
* {@code position2} are close to each other
|
|
||||||
* @see #CLOSE_THRESHOLD
|
* @see #CLOSE_THRESHOLD
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsCloseTo(DistanceRequest)
|
* @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
|
* Returns the next position moved from {@code start} in the direction with {@code angle}, with
|
||||||
* {@code angle}, with step size
|
* step size 0.00015
|
||||||
* 0.00015
|
|
||||||
*
|
*
|
||||||
* @param start The coordinate of the original start point.
|
* @param start The coordinate of the original start point.
|
||||||
* @param angle The direction to be moved in angle.
|
* @param angle The direction to be moved in angle.
|
||||||
|
|
@ -93,19 +90,18 @@ public class GpsCalculationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to check if the given {@code position}
|
* Used to check if the given {@code position} is inside the {@code region}, on edge and vertex
|
||||||
* is inside the {@code region}, on edge and vertex is considered as inside.
|
* is considered as inside.
|
||||||
*
|
*
|
||||||
* @param position The coordinate of the position.
|
* @param position The coordinate of the position.
|
||||||
* @param region A {@link Region} that contains name and a list of
|
* @param region A {@link Region} that contains name and a list of {@code LngLatDto}
|
||||||
* {@code LngLatDto}
|
|
||||||
* @return {@code true} if {@code position} is inside the {@code region}.
|
* @return {@code true} if {@code position} is inside the {@code region}.
|
||||||
* @throws IllegalArgumentException If {@code region} is not closed
|
* @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()
|
* @see Region#isClosed()
|
||||||
*/
|
*/
|
||||||
public boolean checkIsInRegion(LngLat position, Region region)
|
public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException {
|
||||||
throws IllegalArgumentException {
|
|
||||||
if (!region.isClosed()) {
|
if (!region.isClosed()) {
|
||||||
// call method from RegionDto to check if not closed
|
// call method from RegionDto to check if not closed
|
||||||
throw new IllegalArgumentException("Region is 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
|
* Helper function to {@code checkIsInRegion}, use of ray-casting algorithm to check if inside
|
||||||
* to check if inside the polygon
|
* the polygon
|
||||||
*
|
*
|
||||||
* @param point The point to check
|
* @param point The point to check
|
||||||
* @param polygon The region that forms a polygon to check if {@code point}
|
* @param polygon The region that forms a polygon to check if {@code point} sits inside.
|
||||||
* sits inside.
|
* @return If the {@code point} sits inside the {@code polygon} then return {@code true}
|
||||||
* @return If the {@code point} sits inside the {@code polygon} then
|
|
||||||
* return {@code true}
|
|
||||||
* @see #isPointOnEdge(LngLat, LngLat, LngLat)
|
* @see #isPointOnEdge(LngLat, LngLat, LngLat)
|
||||||
* @see #checkIsInRegion(LngLat, Region)
|
* @see #checkIsInRegion(LngLat, Region)
|
||||||
*/
|
*/
|
||||||
|
|
@ -154,9 +148,7 @@ public class GpsCalculationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
double xIntersection =
|
double xIntersection =
|
||||||
a.lng() +
|
a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());
|
||||||
((point.lat() - a.lat()) * (b.lng() - a.lng())) /
|
|
||||||
(b.lat() - a.lat());
|
|
||||||
|
|
||||||
if (xIntersection > point.lng()) {
|
if (xIntersection > point.lng()) {
|
||||||
++intersections;
|
++intersections;
|
||||||
|
|
@ -170,8 +162,7 @@ public class GpsCalculationService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function from {@code rayCasting} that used to simply calculation <br>
|
* Helper function from {@code rayCasting} that used to simply calculation <br>
|
||||||
* Used to check if point {@code p} is on the edge formed by
|
* Used to check if point {@code p} is on the edge formed by {@code a} and {@code b}
|
||||||
* {@code a} and {@code b}
|
|
||||||
*
|
*
|
||||||
* @param p point to be checked on the edge
|
* @param p point to be checked on the edge
|
||||||
* @param a point that forms 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) {
|
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat b) {
|
||||||
// Cross product: (p - a) × (b - a)
|
// Cross product: (p - a) × (b - a)
|
||||||
double crossProduct =
|
double crossProduct =
|
||||||
(p.lng() - a.lng()) * (b.lat() - a.lat()) -
|
(p.lng() - a.lng()) * (b.lat() - a.lat())
|
||||||
(p.lat() - a.lat()) * (b.lng() - a.lng());
|
- (p.lat() - a.lat()) * (b.lng() - a.lng());
|
||||||
if (Math.abs(crossProduct) > 1e-9) {
|
if (Math.abs(crossProduct) > 1e-9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isWithinLng =
|
boolean isWithinLng =
|
||||||
p.lng() >= Math.min(a.lng(), b.lng()) &&
|
p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());
|
||||||
p.lng() <= Math.max(a.lng(), b.lng());
|
|
||||||
boolean isWithinLat =
|
boolean isWithinLat =
|
||||||
p.lat() >= Math.min(a.lat(), b.lat()) &&
|
p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());
|
||||||
p.lat() <= Math.max(a.lat(), b.lat());
|
|
||||||
|
|
||||||
return isWithinLng && isWithinLat;
|
return isWithinLng && isWithinLat;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath;
|
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath;
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
|
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -27,7 +30,6 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles calculations about deliverypath
|
* Class that handles calculations about deliverypath
|
||||||
|
|
@ -40,9 +42,8 @@ import org.springframework.stereotype.Service;
|
||||||
public class PathFinderService {
|
public class PathFinderService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard stop on how many pathfinding iterations we attempt for a single
|
* Hard stop on how many pathfinding iterations we attempt for a single segment before bailing,
|
||||||
* segment before bailing, useful for preventing infinite loops caused by
|
* useful for preventing infinite loops caused by precision quirks or unexpected map data.
|
||||||
* precision quirks or unexpected map data.
|
|
||||||
*
|
*
|
||||||
* @see #computePath(LngLat, LngLat)
|
* @see #computePath(LngLat, LngLat)
|
||||||
*/
|
*/
|
||||||
|
|
@ -59,17 +60,14 @@ public class PathFinderService {
|
||||||
private final List<Region> restrictedRegions;
|
private final List<Region> restrictedRegions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for PathFinderService. The dependencies are injected by
|
* Constructor for PathFinderService. The dependencies are injected by Spring and the
|
||||||
* Spring and the constructor pre-computes reference maps used throughout the
|
* constructor pre-computes reference maps used throughout the request lifecycle.
|
||||||
* request lifecycle.
|
|
||||||
*
|
*
|
||||||
* @param gpsCalculationService Service handling geometric operations.
|
* @param gpsCalculationService Service handling geometric operations.
|
||||||
* @param droneInfoService Service that exposes drone metadata and
|
* @param droneInfoService Service that exposes drone metadata and capability information.
|
||||||
* capability information.
|
|
||||||
*/
|
*/
|
||||||
public PathFinderService(
|
public PathFinderService(
|
||||||
GpsCalculationService gpsCalculationService,
|
GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) {
|
||||||
DroneInfoService droneInfoService) {
|
|
||||||
this.gpsCalculationService = gpsCalculationService;
|
this.gpsCalculationService = gpsCalculationService;
|
||||||
this.droneInfoService = droneInfoService;
|
this.droneInfoService = droneInfoService;
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
|
|
@ -79,11 +77,11 @@ public class PathFinderService {
|
||||||
|
|
||||||
this.drones = droneInfoService.fetchAllDrones();
|
this.drones = droneInfoService.fetchAllDrones();
|
||||||
List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();
|
List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();
|
||||||
List<ServicePointDrones> servicePointAssignments = droneInfoService.fetchDronesForServicePoints();
|
List<ServicePointDrones> servicePointAssignments =
|
||||||
|
droneInfoService.fetchDronesForServicePoints();
|
||||||
List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();
|
List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();
|
||||||
|
|
||||||
this.droneById = this.drones.stream().collect(
|
this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone));
|
||||||
Collectors.toMap(Drone::id, drone -> drone));
|
|
||||||
|
|
||||||
this.droneServicePointMap = new HashMap<>();
|
this.droneServicePointMap = new HashMap<>();
|
||||||
for (ServicePointDrones assignment : servicePointAssignments) {
|
for (ServicePointDrones assignment : servicePointAssignments) {
|
||||||
|
|
@ -94,40 +92,33 @@ public class PathFinderService {
|
||||||
if (availability == null || availability.id() == null) {
|
if (availability == null || availability.id() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
droneServicePointMap.put(
|
droneServicePointMap.put(availability.id(), assignment.servicePointId());
|
||||||
availability.id(),
|
|
||||||
assignment.servicePointId());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.servicePointLocations = servicePoints
|
this.servicePointLocations =
|
||||||
.stream()
|
servicePoints.stream()
|
||||||
.collect(
|
.collect(
|
||||||
Collectors.toMap(ServicePoint::id, sp -> new LngLat(sp.location())));
|
Collectors.toMap(
|
||||||
|
ServicePoint::id, sp -> new LngLat(sp.location())));
|
||||||
|
|
||||||
this.restrictedRegions = restrictedAreas
|
this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList();
|
||||||
.stream()
|
|
||||||
.map(RestrictedArea::toRegion)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce a delivery plan for the provided dispatch records. Deliveries are
|
* Produce a delivery plan for the provided dispatch records. Deliveries are grouped per
|
||||||
* grouped per compatible drone and per trip to satisfy each drone move
|
* compatible drone and per trip to satisfy each drone move limit.
|
||||||
* limit.
|
|
||||||
*
|
*
|
||||||
* @param records Dispatch records to be fulfilled.
|
* @param records Dispatch records to be fulfilled.
|
||||||
* @return Aggregated path response with cost and move totals.
|
* @return Aggregated path response with cost and move totals.
|
||||||
* @see #calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[])
|
* @see #calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[])
|
||||||
*/
|
*/
|
||||||
public DeliveryPathResponse calculateDeliveryPath(
|
public DeliveryPathResponse calculateDeliveryPath(MedDispatchRecRequest[] records) {
|
||||||
MedDispatchRecRequest[] records) {
|
|
||||||
if (records == null || records.length == 0) {
|
if (records == null || records.length == 0) {
|
||||||
return new DeliveryPathResponse(0f, 0, new DronePath[0]);
|
return new DeliveryPathResponse(0f, 0, new DronePath[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<MedDispatchRecRequest>> assigned = assignDeliveries(
|
Map<String, List<MedDispatchRecRequest>> assigned = assignDeliveries(records);
|
||||||
records);
|
|
||||||
|
|
||||||
List<DronePath> paths = new ArrayList<>();
|
List<DronePath> paths = new ArrayList<>();
|
||||||
float totalCost = 0f;
|
float totalCost = 0f;
|
||||||
|
|
@ -148,25 +139,20 @@ public class PathFinderService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MedDispatchRecRequest> sortedDeliveries = entry
|
List<MedDispatchRecRequest> sortedDeliveries =
|
||||||
.getValue()
|
entry.getValue().stream()
|
||||||
.stream()
|
.sorted(
|
||||||
.sorted(
|
Comparator.comparingDouble(
|
||||||
Comparator.comparingDouble(rec -> gpsCalculationService.calculateDistance(
|
rec ->
|
||||||
servicePointLocation,
|
gpsCalculationService.calculateDistance(
|
||||||
rec.delivery())))
|
servicePointLocation, rec.delivery())))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<List<MedDispatchRecRequest>> trips = splitTrips(
|
List<List<MedDispatchRecRequest>> trips =
|
||||||
sortedDeliveries,
|
splitTrips(sortedDeliveries, drone, servicePointLocation);
|
||||||
drone,
|
|
||||||
servicePointLocation);
|
|
||||||
|
|
||||||
for (List<MedDispatchRecRequest> trip : trips) {
|
for (List<MedDispatchRecRequest> trip : trips) {
|
||||||
TripResult result = buildTrip(
|
TripResult result = buildTrip(drone, servicePointLocation, trip);
|
||||||
drone,
|
|
||||||
servicePointLocation,
|
|
||||||
trip);
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
totalCost += result.cost();
|
totalCost += result.cost();
|
||||||
totalMoves += result.moves();
|
totalMoves += result.moves();
|
||||||
|
|
@ -175,23 +161,18 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DeliveryPathResponse(
|
return new DeliveryPathResponse(totalCost, totalMoves, paths.toArray(new DronePath[0]));
|
||||||
totalCost,
|
|
||||||
totalMoves,
|
|
||||||
paths.toArray(new DronePath[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience wrapper around {@link #calculateDeliveryPath} that serializes
|
* Convenience wrapper around {@link #calculateDeliveryPath} that serializes the result into a
|
||||||
* the result into a GeoJSON FeatureCollection suitable for mapping
|
* GeoJSON FeatureCollection suitable for mapping visualization.
|
||||||
* visualization.
|
|
||||||
*
|
*
|
||||||
* @param records Dispatch records to be fulfilled.
|
* @param records Dispatch records to be fulfilled.
|
||||||
* @return GeoJSON payload representing every delivery flight path.
|
* @return GeoJSON payload representing every delivery flight path.
|
||||||
* @throws IllegalStateException When the payload cannot be serialized.
|
* @throws IllegalStateException When the payload cannot be serialized.
|
||||||
*/
|
*/
|
||||||
public String calculateDeliveryPathAsGeoJson(
|
public String calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[] records) {
|
||||||
MedDispatchRecRequest[] records) {
|
|
||||||
DeliveryPathResponse response = calculateDeliveryPath(records);
|
DeliveryPathResponse response = calculateDeliveryPath(records);
|
||||||
Map<String, Object> featureCollection = new LinkedHashMap<>();
|
Map<String, Object> featureCollection = new LinkedHashMap<>();
|
||||||
featureCollection.put("type", "FeatureCollection");
|
featureCollection.put("type", "FeatureCollection");
|
||||||
|
|
@ -232,16 +213,13 @@ public class PathFinderService {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(featureCollection);
|
return objectMapper.writeValueAsString(featureCollection);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException("Failed to generate GeoJSON payload", e);
|
||||||
"Failed to generate GeoJSON payload",
|
|
||||||
e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group dispatch records by their assigned drone, ensuring every record is
|
* Group dispatch records by their assigned drone, ensuring every record is routed through
|
||||||
* routed through {@link #findBestDrone(MedDispatchRecRequest)} exactly once
|
* {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries.
|
||||||
* and discarding invalid entries.
|
|
||||||
*
|
*
|
||||||
* @param records Dispatch records to be grouped.
|
* @param records Dispatch records to be grouped.
|
||||||
* @return Map keyed by drone ID with the deliveries it should service.
|
* @return Map keyed by drone ID with the deliveries it should service.
|
||||||
|
|
@ -254,16 +232,14 @@ public class PathFinderService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String droneId = findBestDrone(record);
|
String droneId = findBestDrone(record);
|
||||||
assignments
|
assignments.computeIfAbsent(droneId, id -> new ArrayList<>()).add(record);
|
||||||
.computeIfAbsent(droneId, id -> new ArrayList<>())
|
|
||||||
.add(record);
|
|
||||||
}
|
}
|
||||||
return assignments;
|
return assignments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Choose the best drone for the provided record. Currently that equates to
|
* Choose the best drone for the provided record. Currently that equates to picking the closest
|
||||||
* picking the closest compatible drone to the delivery location.
|
* compatible drone to the delivery location.
|
||||||
*
|
*
|
||||||
* @param record Dispatch record that needs fulfillment.
|
* @param record Dispatch record that needs fulfillment.
|
||||||
* @return Identifier of the drone that should fly the mission.
|
* @return Identifier of the drone that should fly the mission.
|
||||||
|
|
@ -282,15 +258,14 @@ public class PathFinderService {
|
||||||
if (servicePointId == null) {
|
if (servicePointId == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LngLat servicePointLocation = servicePointLocations.get(
|
LngLat servicePointLocation = servicePointLocations.get(servicePointId);
|
||||||
servicePointId);
|
|
||||||
if (servicePointLocation == null) {
|
if (servicePointLocation == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = gpsCalculationService.calculateDistance(
|
double distance =
|
||||||
servicePointLocation,
|
gpsCalculationService.calculateDistance(
|
||||||
record.delivery());
|
servicePointLocation, record.delivery());
|
||||||
|
|
||||||
if (distance < bestScore) {
|
if (distance < bestScore) {
|
||||||
bestScore = distance;
|
bestScore = distance;
|
||||||
|
|
@ -298,28 +273,23 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bestDrone == null) {
|
if (bestDrone == null) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException("No available drone for delivery " + record.id());
|
||||||
"No available drone for delivery " + record.id());
|
|
||||||
}
|
}
|
||||||
return bestDrone;
|
return bestDrone;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Break a sequence of deliveries into several trips that each respect the
|
* Break a sequence of deliveries into several trips that each respect the drone move limit. The
|
||||||
* drone move limit. The deliveries should already be ordered by proximity
|
* deliveries should already be ordered by proximity for sensible grouping.
|
||||||
* for sensible grouping.
|
|
||||||
*
|
*
|
||||||
* @param deliveries Deliveries assigned to a drone.
|
* @param deliveries Deliveries assigned to a drone.
|
||||||
* @param drone Drone that will service the deliveries.
|
* @param drone Drone that will service the deliveries.
|
||||||
* @param servicePoint Starting and ending point of every trip.
|
* @param servicePoint Starting and ending point of every trip.
|
||||||
* @return Partitioned trips with at least one delivery each.
|
* @return Partitioned trips with at least one delivery each.
|
||||||
* @throws IllegalStateException If a single delivery exceeds the drone's
|
* @throws IllegalStateException If a single delivery exceeds the drone's move limit.
|
||||||
* move limit.
|
|
||||||
*/
|
*/
|
||||||
private List<List<MedDispatchRecRequest>> splitTrips(
|
private List<List<MedDispatchRecRequest>> splitTrips(
|
||||||
List<MedDispatchRecRequest> deliveries,
|
List<MedDispatchRecRequest> deliveries, Drone drone, LngLat servicePoint) {
|
||||||
Drone drone,
|
|
||||||
LngLat servicePoint) {
|
|
||||||
List<List<MedDispatchRecRequest>> trips = new ArrayList<>();
|
List<List<MedDispatchRecRequest>> trips = new ArrayList<>();
|
||||||
List<MedDispatchRecRequest> currentTrip = new ArrayList<>();
|
List<MedDispatchRecRequest> currentTrip = new ArrayList<>();
|
||||||
for (MedDispatchRecRequest delivery : deliveries) {
|
for (MedDispatchRecRequest delivery : deliveries) {
|
||||||
|
|
@ -329,11 +299,11 @@ public class PathFinderService {
|
||||||
currentTrip.remove(currentTrip.size() - 1);
|
currentTrip.remove(currentTrip.size() - 1);
|
||||||
if (currentTrip.isEmpty()) {
|
if (currentTrip.isEmpty()) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Delivery " +
|
"Delivery "
|
||||||
delivery.id() +
|
+ delivery.id()
|
||||||
" exceeds drone " +
|
+ " exceeds drone "
|
||||||
drone.id() +
|
+ drone.id()
|
||||||
" move limit");
|
+ " move limit");
|
||||||
}
|
}
|
||||||
trips.add(new ArrayList<>(currentTrip));
|
trips.add(new ArrayList<>(currentTrip));
|
||||||
currentTrip.clear();
|
currentTrip.clear();
|
||||||
|
|
@ -352,20 +322,18 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a single trip for the provided drone, including the entire flight
|
* Build a single trip for the provided drone, including the entire flight path to every
|
||||||
* path to every delivery and back home. The resulting structure contains the
|
* delivery and back home. The resulting structure contains the {@link DronePath} representation
|
||||||
* {@link DronePath} representation as well as cost and moves consumed.
|
* 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 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.
|
* @return Trip information or {@code null} if no deliveries are provided.
|
||||||
* @see DeliveryPathResponse.DronePath
|
* @see DeliveryPathResponse.DronePath
|
||||||
*/
|
*/
|
||||||
private TripResult buildTrip(
|
private TripResult buildTrip(
|
||||||
Drone drone,
|
Drone drone, LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
||||||
LngLat servicePoint,
|
|
||||||
List<MedDispatchRecRequest> deliveries) {
|
|
||||||
if (deliveries == null || deliveries.isEmpty()) {
|
if (deliveries == null || deliveries.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -390,9 +358,7 @@ public class PathFinderService {
|
||||||
moves += toDelivery.moves();
|
moves += toDelivery.moves();
|
||||||
|
|
||||||
if (i == deliveries.size() - 1) {
|
if (i == deliveries.size() - 1) {
|
||||||
PathSegment backHome = computePath(
|
PathSegment backHome = computePath(delivery.delivery(), servicePoint);
|
||||||
delivery.delivery(),
|
|
||||||
servicePoint);
|
|
||||||
backHome.appendSkippingStart(flightPath);
|
backHome.appendSkippingStart(flightPath);
|
||||||
moves += backHome.moves();
|
moves += backHome.moves();
|
||||||
current = servicePoint;
|
current = servicePoint;
|
||||||
|
|
@ -402,9 +368,10 @@ public class PathFinderService {
|
||||||
flightPlans.add(new Delivery(delivery.id(), flightPath));
|
flightPlans.add(new Delivery(delivery.id(), flightPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
float cost = drone.capability().costInitial() +
|
float cost =
|
||||||
drone.capability().costFinal() +
|
drone.capability().costInitial()
|
||||||
(float) (drone.capability().costPerMove() * moves);
|
+ drone.capability().costFinal()
|
||||||
|
+ (float) (drone.capability().costPerMove() * moves);
|
||||||
|
|
||||||
DronePath path = new DronePath(drone.parseId(), flightPlans);
|
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
|
* Estimate the number of moves a prospective trip would need by replaying the path calculation
|
||||||
* the path calculation without mutating any persistent state.
|
* without mutating any persistent state.
|
||||||
*
|
*
|
||||||
* @param servicePoint Trip origin.
|
* @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.
|
* @return Total moves required to fly the proposed itinerary.
|
||||||
*/
|
*/
|
||||||
private int estimateTripMoves(
|
private int estimateTripMoves(LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
||||||
LngLat servicePoint,
|
|
||||||
List<MedDispatchRecRequest> deliveries) {
|
|
||||||
if (deliveries.isEmpty()) {
|
if (deliveries.isEmpty()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -437,10 +402,10 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a path between {@code start} and {@code target} by repeatedly moving
|
* Build a path between {@code start} and {@code target} by repeatedly moving in snapped
|
||||||
* in snapped increments while avoiding restricted zones.
|
* increments while avoiding restricted zones.
|
||||||
*
|
*
|
||||||
* @param start Start coordinate.
|
* @param start Start coordinate.
|
||||||
* @param target Destination coordinate.
|
* @param target Destination coordinate.
|
||||||
* @return Sequence of visited coordinates and move count.
|
* @return Sequence of visited coordinates and move count.
|
||||||
* @see #nextPosition(LngLat, LngLat)
|
* @see #nextPosition(LngLat, LngLat)
|
||||||
|
|
@ -453,8 +418,8 @@ public class PathFinderService {
|
||||||
positions.add(start);
|
positions.add(start);
|
||||||
LngLat current = start;
|
LngLat current = start;
|
||||||
int iterations = 0;
|
int iterations = 0;
|
||||||
while (!gpsCalculationService.isCloseTo(current, target) &&
|
while (!gpsCalculationService.isCloseTo(current, target)
|
||||||
iterations < MAX_SEGMENT_ITERATIONS) {
|
&& iterations < MAX_SEGMENT_ITERATIONS) {
|
||||||
LngLat next = nextPosition(current, target);
|
LngLat next = nextPosition(current, target);
|
||||||
if (next.isSamePoint(current)) {
|
if (next.isSamePoint(current)) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -470,20 +435,18 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the next position on the path from {@code current} toward
|
* Determine the next position on the path from {@code current} toward {@code target},
|
||||||
* {@code target}, preferring the snapped angle closest to the desired
|
* preferring the snapped angle closest to the desired heading that does not infiltrate a
|
||||||
* heading that does not infiltrate a restricted region.
|
* restricted region.
|
||||||
*
|
*
|
||||||
* @param current Current coordinate.
|
* @param current Current coordinate.
|
||||||
* @param target Destination coordinate.
|
* @param target Destination coordinate.
|
||||||
* @return Next admissible coordinate or the original point if none can be
|
* @return Next admissible coordinate or the original point if none can be found.
|
||||||
* found.
|
|
||||||
*/
|
*/
|
||||||
private LngLat nextPosition(LngLat current, LngLat target) {
|
private LngLat nextPosition(LngLat current, LngLat target) {
|
||||||
double desiredAngle = Math.toDegrees(
|
double desiredAngle =
|
||||||
Math.atan2(
|
Math.toDegrees(
|
||||||
target.lat() - current.lat(),
|
Math.atan2(target.lat() - current.lat(), target.lng() - current.lng()));
|
||||||
target.lng() - current.lng()));
|
|
||||||
List<Angle> candidateAngles = buildAngleCandidates(desiredAngle);
|
List<Angle> candidateAngles = buildAngleCandidates(desiredAngle);
|
||||||
for (Angle angle : candidateAngles) {
|
for (Angle angle : candidateAngles) {
|
||||||
LngLat next = gpsCalculationService.nextPosition(current, angle);
|
LngLat next = gpsCalculationService.nextPosition(current, angle);
|
||||||
|
|
@ -495,12 +458,11 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a sequence of candidate angles centered on the desired heading,
|
* Build a sequence of candidate angles centered on the desired heading, expanding symmetrically
|
||||||
* expanding symmetrically clockwise and counter-clockwise to explore
|
* clockwise and counter-clockwise to explore alternative headings if the primary path is
|
||||||
* alternative headings if the primary path is blocked.
|
* blocked.
|
||||||
*
|
*
|
||||||
* @param desiredAngle Bearing in degrees between current and target
|
* @param desiredAngle Bearing in degrees between current and target positions.
|
||||||
* positions.
|
|
||||||
* @return Ordered list of candidate snapped angles.
|
* @return Ordered list of candidate snapped angles.
|
||||||
* @see Angle#snap(double)
|
* @see Angle#snap(double)
|
||||||
*/
|
*/
|
||||||
|
|
@ -532,17 +494,16 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a computed path segment wrapping the visited positions
|
* Representation of a computed path segment wrapping the visited positions and the number of
|
||||||
* and the number of moves taken to traverse them.
|
* moves taken to traverse them.
|
||||||
*
|
*
|
||||||
* @param positions Ordered coordinates that describe the path.
|
* @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<LngLat> positions, int moves) {
|
private record PathSegment(List<LngLat> positions, int moves) {
|
||||||
/**
|
/**
|
||||||
* Append the positions from this segment to {@code target}, skipping the
|
* Append the positions from this segment to {@code target}, skipping the first coordinate
|
||||||
* first coordinate as it is already represented by the last coordinate
|
* as it is already represented by the last coordinate in the consumer path.
|
||||||
* in the consumer path.
|
|
||||||
*
|
*
|
||||||
* @param target Mutable list to append to.
|
* @param target Mutable list to append to.
|
||||||
*/
|
*/
|
||||||
|
|
@ -554,9 +515,8 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundle containing the calculated {@link DronePath}, total moves and
|
* Bundle containing the calculated {@link DronePath}, total moves and financial cost for a
|
||||||
* financial cost for a single trip.
|
* single trip.
|
||||||
*/
|
*/
|
||||||
private record TripResult(DronePath path, int moves, float cost) {
|
private record TripResult(DronePath path, int moves, float cost) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
package io.github.js0ny.ilp_coursework.util;
|
package io.github.js0ny.ilp_coursework.util;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator for attribute values in {@code JsonNode}.
|
* Comparator for attribute values in {@code JsonNode}.
|
||||||
*
|
*
|
||||||
* This is a helper for dynamic querying.
|
* <p>This is a helper for dynamic querying.
|
||||||
*/
|
*/
|
||||||
public class AttrComparator {
|
public class AttrComparator {
|
||||||
/**
|
/**
|
||||||
* Helper for dynamic querying, to compare the json value with given value in
|
* Helper for dynamic querying, to compare the json value with given value in {@code String}.
|
||||||
* {@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 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.
|
* @return {@code true} if given values are equal, otherwise false.
|
||||||
*/
|
*/
|
||||||
public static boolean isValueMatched(JsonNode node, String attrVal, AttrOperator op) {
|
public static boolean isValueMatched(JsonNode node, String attrVal, AttrOperator op) {
|
||||||
|
|
@ -46,9 +45,9 @@ public class AttrComparator {
|
||||||
return switch (op) {
|
return switch (op) {
|
||||||
case EQ -> nodeVal.equals(attrVal);
|
case EQ -> nodeVal.equals(attrVal);
|
||||||
default -> !nodeVal.equals(attrVal);
|
default -> !nodeVal.equals(attrVal);
|
||||||
// case NE -> !nodeVal.equals(attrVal);
|
// case NE -> !nodeVal.equals(attrVal);
|
||||||
// case GT -> !nodeVal.equals(attrVal);// > 0;
|
// case GT -> !nodeVal.equals(attrVal);// > 0;
|
||||||
// case LT -> !nodeVal.equals(attrVal);// < 0;
|
// case LT -> !nodeVal.equals(attrVal);// < 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +55,9 @@ public class AttrComparator {
|
||||||
return switch (op) {
|
return switch (op) {
|
||||||
case EQ -> nodeVal == attrVal;
|
case EQ -> nodeVal == attrVal;
|
||||||
default -> nodeVal != attrVal;
|
default -> nodeVal != attrVal;
|
||||||
// case NE -> nodeVal != attrVal;
|
// case NE -> nodeVal != attrVal;
|
||||||
// case GT -> !nodeVal && attrVal; // false < true
|
// case GT -> !nodeVal && attrVal; // false < true
|
||||||
// case LT -> nodeVal && !attrVal; // true > false
|
// case LT -> nodeVal && !attrVal; // true > false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
package io.github.js0ny.ilp_coursework;
|
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.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
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
|
@SpringBootTest
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
public class ActuatorHealthTest {
|
public class ActuatorHealthTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private MockMvc mockMvc;
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("GET /actuator/health -> 200 OK")
|
@DisplayName("GET /actuator/health -> 200 OK")
|
||||||
void getActuator_shouldReturn200AndON() throws Exception {
|
void getActuator_shouldReturn200AndON() throws Exception {
|
||||||
String endpoint = "/actuator/health";
|
String endpoint = "/actuator/health";
|
||||||
String expected = """
|
String expected =
|
||||||
|
"""
|
||||||
{
|
{
|
||||||
"status": "UP"
|
"status": "UP"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class IlpCourseworkApplicationTests {
|
class IlpCourseworkApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Angle;
|
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.LngLat;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
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.MovementRequest;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||||
import java.util.List;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
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.MockMvc;
|
||||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@WebMvcTest(ApiController.class)
|
@WebMvcTest(ApiController.class)
|
||||||
public class ApiControllerTest {
|
public class ApiControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private MockMvc mockMvc;
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired private ObjectMapper objectMapper;
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean private GpsCalculationService service;
|
||||||
private GpsCalculationService service;
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("GET /uid")
|
@DisplayName("GET /uid")
|
||||||
|
|
@ -59,21 +59,19 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /distanceTo -> 200 OK")
|
@DisplayName("POST /distanceTo -> 200 OK")
|
||||||
void getDistance_shouldReturn200AndDistance_whenCorrectInput()
|
void getDistance_shouldReturn200AndDistance_whenCorrectInput() throws Exception {
|
||||||
throws Exception {
|
|
||||||
double expected = 5.0;
|
double expected = 5.0;
|
||||||
String endpoint = "/api/v1/distanceTo";
|
String endpoint = "/api/v1/distanceTo";
|
||||||
LngLat p1 = new LngLat(0, 4.0);
|
LngLat p1 = new LngLat(0, 4.0);
|
||||||
LngLat p2 = new LngLat(3.0, 0);
|
LngLat p2 = new LngLat(3.0, 0);
|
||||||
var req = new DistanceRequest(p1, p2);
|
var req = new DistanceRequest(p1, p2);
|
||||||
when(
|
when(service.calculateDistance(any(LngLat.class), any(LngLat.class)))
|
||||||
service.calculateDistance(any(LngLat.class), any(LngLat.class))
|
.thenReturn(expected);
|
||||||
).thenReturn(expected);
|
var mock =
|
||||||
var mock = mockMvc.perform(
|
mockMvc.perform(
|
||||||
post(endpoint)
|
post(endpoint)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(req))
|
.content(objectMapper.writeValueAsString(req)));
|
||||||
);
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(content().string(String.valueOf(expected)));
|
mock.andExpect(content().string(String.valueOf(expected)));
|
||||||
|
|
@ -83,43 +81,33 @@ public class ApiControllerTest {
|
||||||
@DisplayName("POST /distanceTo -> 400 Bad Request: Missing Field")
|
@DisplayName("POST /distanceTo -> 400 Bad Request: Missing Field")
|
||||||
void getDistance_shouldReturn400_whenMissingField() throws Exception {
|
void getDistance_shouldReturn400_whenMissingField() throws Exception {
|
||||||
String endpoint = "/api/v1/distanceTo";
|
String endpoint = "/api/v1/distanceTo";
|
||||||
String req = """
|
String req =
|
||||||
{
|
"""
|
||||||
"position1": {
|
{
|
||||||
"lng": 3.0,
|
"position1": {
|
||||||
"lat": 4.0
|
"lng": 3.0,
|
||||||
|
"lat": 4.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
""";
|
||||||
""";
|
when(service.calculateDistance(any(LngLat.class), isNull()))
|
||||||
when(
|
.thenThrow(new NullPointerException());
|
||||||
service.calculateDistance(any(LngLat.class), isNull())
|
mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
|
||||||
).thenThrow(new NullPointerException());
|
.andExpect(status().isBadRequest());
|
||||||
mockMvc
|
|
||||||
.perform(
|
|
||||||
post(endpoint)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(req)
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /distanceTo -> 400 Bad Request: Semantic errors")
|
@DisplayName("POST /distanceTo -> 400 Bad Request: Semantic errors")
|
||||||
void getDistance_shouldReturn400_whenInvalidInput() throws Exception {
|
void getDistance_shouldReturn400_whenInvalidInput() throws Exception {
|
||||||
String endpoint = "/api/v1/distanceTo";
|
String endpoint = "/api/v1/distanceTo";
|
||||||
String req = """
|
String req =
|
||||||
{ "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } }
|
"""
|
||||||
""";
|
{ "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } }
|
||||||
when(
|
""";
|
||||||
service.calculateDistance(any(LngLat.class), isNull())
|
when(service.calculateDistance(any(LngLat.class), isNull()))
|
||||||
).thenThrow(new NullPointerException());
|
.thenThrow(new NullPointerException());
|
||||||
mockMvc
|
mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
|
||||||
.perform(
|
.andExpect(status().isBadRequest());
|
||||||
post(endpoint)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(req)
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,21 +117,18 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /isCloseTo -> 200 OK")
|
@DisplayName("POST /isCloseTo -> 200 OK")
|
||||||
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput()
|
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
|
||||||
throws Exception {
|
|
||||||
boolean expected = false;
|
boolean expected = false;
|
||||||
String endpoint = "/api/v1/isCloseTo";
|
String endpoint = "/api/v1/isCloseTo";
|
||||||
LngLat p1 = new LngLat(0, 4.0);
|
LngLat p1 = new LngLat(0, 4.0);
|
||||||
LngLat p2 = new LngLat(3.0, 0);
|
LngLat p2 = new LngLat(3.0, 0);
|
||||||
var req = new DistanceRequest(p1, p2);
|
var req = new DistanceRequest(p1, p2);
|
||||||
when(
|
when(service.isCloseTo(any(LngLat.class), any(LngLat.class))).thenReturn(expected);
|
||||||
service.isCloseTo(any(LngLat.class), any(LngLat.class))
|
var mock =
|
||||||
).thenReturn(expected);
|
mockMvc.perform(
|
||||||
var mock = mockMvc.perform(
|
post(endpoint)
|
||||||
post(endpoint)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.content(objectMapper.writeValueAsString(req)));
|
||||||
.content(objectMapper.writeValueAsString(req))
|
|
||||||
);
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(content().string(String.valueOf(expected)));
|
mock.andExpect(content().string(String.valueOf(expected)));
|
||||||
|
|
@ -151,39 +136,32 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
|
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
|
||||||
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed()
|
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() throws Exception {
|
||||||
throws Exception {
|
|
||||||
// json without a bracket
|
// json without a bracket
|
||||||
String malformedJson = """
|
String malformedJson =
|
||||||
{
|
"""
|
||||||
"position1": { "lng": 0.0, "lat": 3.0 }
|
{
|
||||||
""";
|
"position1": { "lng": 0.0, "lat": 3.0 }
|
||||||
mockMvc
|
""";
|
||||||
.perform(
|
mockMvc.perform(
|
||||||
post("/api/v1/isCloseTo")
|
post("/api/v1/isCloseTo")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(malformedJson)
|
.content(malformedJson))
|
||||||
)
|
.andExpect(status().isBadRequest());
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /isCloseTo -> 400 Bad Request: Semantic errors")
|
@DisplayName("POST /isCloseTo -> 400 Bad Request: Semantic errors")
|
||||||
void getIsCloseTo_shouldReturn400_whenInvalidInput() throws Exception {
|
void getIsCloseTo_shouldReturn400_whenInvalidInput() throws Exception {
|
||||||
String endpoint = "/api/v1/isCloseTo";
|
String endpoint = "/api/v1/isCloseTo";
|
||||||
String req = """
|
String req =
|
||||||
{ "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } }
|
"""
|
||||||
|
{ "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } }
|
||||||
""";
|
""";
|
||||||
when(
|
when(service.calculateDistance(any(LngLat.class), isNull()))
|
||||||
service.calculateDistance(any(LngLat.class), isNull())
|
.thenThrow(new NullPointerException());
|
||||||
).thenThrow(new NullPointerException());
|
mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
|
||||||
mockMvc
|
.andExpect(status().isBadRequest());
|
||||||
.perform(
|
|
||||||
post(endpoint)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(req)
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,73 +173,59 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /nextPosition -> 200 OK")
|
@DisplayName("POST /nextPosition -> 200 OK")
|
||||||
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput()
|
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() throws Exception {
|
||||||
throws Exception {
|
|
||||||
LngLat expected = new LngLat(0.00015, 0.0);
|
LngLat expected = new LngLat(0.00015, 0.0);
|
||||||
LngLat p = new LngLat(0, 0);
|
LngLat p = new LngLat(0, 0);
|
||||||
var req = new MovementRequest(p, 0);
|
var req = new MovementRequest(p, 0);
|
||||||
when(
|
when(service.nextPosition(any(LngLat.class), any(Angle.class))).thenReturn(expected);
|
||||||
service.nextPosition(any(LngLat.class), any(Angle.class))
|
var mock =
|
||||||
).thenReturn(expected);
|
mockMvc.perform(
|
||||||
var mock = mockMvc.perform(
|
post(endpoint)
|
||||||
post(endpoint)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.content(objectMapper.writeValueAsString(req)));
|
||||||
.content(objectMapper.writeValueAsString(req))
|
|
||||||
);
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(
|
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
content().json(objectMapper.writeValueAsString(expected))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
|
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
|
||||||
void getNextPosition_shouldReturn400_whenKeyNameError()
|
void getNextPosition_shouldReturn400_whenKeyNameError() throws Exception {
|
||||||
throws Exception {
|
|
||||||
// "position" should be "start"
|
// "position" should be "start"
|
||||||
String malformedJson = """
|
String malformedJson =
|
||||||
{
|
"""
|
||||||
"position": { "lng": 0.0, "lat": 3.0 },
|
{
|
||||||
"angle": 180
|
"position": { "lng": 0.0, "lat": 3.0 },
|
||||||
}
|
"angle": 180
|
||||||
""";
|
}
|
||||||
when(service.nextPosition(isNull(), any(Angle.class))).thenThrow(
|
""";
|
||||||
new NullPointerException()
|
when(service.nextPosition(isNull(), any(Angle.class)))
|
||||||
);
|
.thenThrow(new NullPointerException());
|
||||||
mockMvc
|
mockMvc.perform(
|
||||||
.perform(
|
post("/api/v1/nextPosition")
|
||||||
post("/api/v1/nextPosition")
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.content(malformedJson))
|
||||||
.content(malformedJson)
|
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
||||||
)
|
|
||||||
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /nextPosition -> 400 Bad Request: Semantic errors")
|
@DisplayName("POST /nextPosition -> 400 Bad Request: Semantic errors")
|
||||||
void getNextPosition_shouldReturn400_whenInvalidInput()
|
void getNextPosition_shouldReturn400_whenInvalidInput() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String endpoint = "/api/v1/nextPosition";
|
String endpoint = "/api/v1/nextPosition";
|
||||||
String req = """
|
String req =
|
||||||
{
|
"""
|
||||||
"start": {
|
{
|
||||||
"lng": -3.192473,
|
"start": {
|
||||||
"lat": 55.946233
|
"lng": -3.192473,
|
||||||
},
|
"lat": 55.946233
|
||||||
"angle": 900
|
},
|
||||||
}
|
"angle": 900
|
||||||
""";
|
}
|
||||||
when(
|
""";
|
||||||
service.calculateDistance(any(LngLat.class), isNull())
|
when(service.calculateDistance(any(LngLat.class), isNull()))
|
||||||
).thenThrow(new NullPointerException());
|
.thenThrow(new NullPointerException());
|
||||||
mockMvc
|
mockMvc.perform(post(endpoint).contentType(MediaType.APPLICATION_JSON).content(req))
|
||||||
.perform(
|
.andExpect(status().isBadRequest());
|
||||||
post(endpoint)
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(req)
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,30 +235,27 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("POST /isInRegion -> 200 OK")
|
@DisplayName("POST /isInRegion -> 200 OK")
|
||||||
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput()
|
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() throws Exception {
|
||||||
throws Exception {
|
|
||||||
boolean expected = false;
|
boolean expected = false;
|
||||||
String endpoint = "/api/v1/isInRegion";
|
String endpoint = "/api/v1/isInRegion";
|
||||||
var position = new LngLat(1.234, 1.222);
|
var position = new LngLat(1.234, 1.222);
|
||||||
var region = new Region(
|
var region =
|
||||||
"central",
|
new Region(
|
||||||
List.of(
|
"central",
|
||||||
new LngLat(-3.192473, 55.946233),
|
List.of(
|
||||||
new LngLat(-3.192473, 55.942617),
|
new LngLat(-3.192473, 55.946233),
|
||||||
new LngLat(-3.184319, 55.942617),
|
new LngLat(-3.192473, 55.942617),
|
||||||
new LngLat(-3.184319, 55.946233),
|
new LngLat(-3.184319, 55.942617),
|
||||||
new LngLat(-3.192473, 55.946233)
|
new LngLat(-3.184319, 55.946233),
|
||||||
)
|
new LngLat(-3.192473, 55.946233)));
|
||||||
);
|
|
||||||
var req = new RegionCheckRequest(position, region);
|
var req = new RegionCheckRequest(position, region);
|
||||||
when(
|
when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
|
||||||
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
.thenReturn(expected);
|
||||||
).thenReturn(expected);
|
var mock =
|
||||||
var mock = mockMvc.perform(
|
mockMvc.perform(
|
||||||
post(endpoint)
|
post(endpoint)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(objectMapper.writeValueAsString(req))
|
.content(objectMapper.writeValueAsString(req)));
|
||||||
);
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(content().string(String.valueOf(expected)));
|
mock.andExpect(content().string(String.valueOf(expected)));
|
||||||
|
|
@ -302,53 +263,44 @@ public class ApiControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName(
|
||||||
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
|
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to"
|
||||||
)
|
+ " isInRegion")
|
||||||
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments()
|
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments() throws Exception {
|
||||||
throws Exception {
|
|
||||||
var position = new LngLat(1, 1);
|
var position = new LngLat(1, 1);
|
||||||
var region = new Region("illegal", List.of());
|
var region = new Region("illegal", List.of());
|
||||||
var request = new RegionCheckRequest(position, region);
|
var request = new RegionCheckRequest(position, region);
|
||||||
when(
|
when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
|
||||||
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
.thenThrow(new IllegalArgumentException("Region is not closed."));
|
||||||
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
mockMvc.perform(
|
||||||
mockMvc
|
post("/api/v1/isInRegion")
|
||||||
.perform(
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
post("/api/v1/isInRegion")
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.andExpect(status().isBadRequest());
|
||||||
.content(objectMapper.writeValueAsString(request))
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName(
|
||||||
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
|
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to"
|
||||||
)
|
+ " isInRegion")
|
||||||
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices()
|
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices() throws Exception {
|
||||||
throws Exception {
|
|
||||||
var position = new LngLat(1, 1);
|
var position = new LngLat(1, 1);
|
||||||
var region = new Region(
|
var region =
|
||||||
"illegal",
|
new Region(
|
||||||
List.of(
|
"illegal",
|
||||||
new LngLat(1, 2),
|
List.of(
|
||||||
new LngLat(3, 4),
|
new LngLat(1, 2),
|
||||||
new LngLat(5, 6),
|
new LngLat(3, 4),
|
||||||
new LngLat(7, 8),
|
new LngLat(5, 6),
|
||||||
new LngLat(9, 10)
|
new LngLat(7, 8),
|
||||||
)
|
new LngLat(9, 10)));
|
||||||
);
|
|
||||||
var request = new RegionCheckRequest(position, region);
|
var request = new RegionCheckRequest(position, region);
|
||||||
when(
|
when(service.checkIsInRegion(any(LngLat.class), any(Region.class)))
|
||||||
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
.thenThrow(new IllegalArgumentException("Region is not closed."));
|
||||||
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
mockMvc.perform(
|
||||||
mockMvc
|
post("/api/v1/isInRegion")
|
||||||
.perform(
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
post("/api/v1/isInRegion")
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.andExpect(status().isBadRequest());
|
||||||
.content(objectMapper.writeValueAsString(request))
|
|
||||||
)
|
|
||||||
.andExpect(status().isBadRequest());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
|
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.common.LngLat;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
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.DroneAttrComparatorService;
|
||||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||||
import io.github.js0ny.ilp_coursework.service.PathFinderService;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
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.context.bean.override.mockito.MockitoBean;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
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)
|
@WebMvcTest(DroneController.class)
|
||||||
public class DroneControllerTest {
|
public class DroneControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private MockMvc mockMvc;
|
||||||
private MockMvc mockMvc;
|
|
||||||
|
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean private DroneInfoService droneInfoService;
|
||||||
private DroneInfoService droneInfoService;
|
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean private DroneAttrComparatorService droneAttrComparatorService;
|
||||||
private DroneAttrComparatorService droneAttrComparatorService;
|
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean private PathFinderService pathFinderService;
|
||||||
private PathFinderService pathFinderService;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
|
@ -66,14 +65,11 @@ public class DroneControllerTest {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String endpoint = API_ENDPOINT_BASE + "true";
|
String endpoint = API_ENDPOINT_BASE + "true";
|
||||||
List<String> expected = List.of("1", "5", "8", "9");
|
List<String> expected = List.of("1", "5", "8", "9");
|
||||||
when(
|
when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
|
||||||
droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
|
var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
||||||
var mock = mockMvc.perform(
|
|
||||||
get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(
|
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
content().json(objectMapper.writeValueAsString(expected)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -82,23 +78,18 @@ public class DroneControllerTest {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String endpoint = API_ENDPOINT_BASE + "false";
|
String endpoint = API_ENDPOINT_BASE + "false";
|
||||||
List<String> expected = List.of("2", "3", "4", "6", "7", "10");
|
List<String> expected = List.of("2", "3", "4", "6", "7", "10");
|
||||||
when(
|
when(droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
|
||||||
droneInfoService.dronesWithCooling(anyBoolean())).thenReturn(expected);
|
var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
||||||
var mock = mockMvc.perform(
|
|
||||||
get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(
|
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
content().json(objectMapper.writeValueAsString(expected)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("-> 400 Bad Request")
|
@DisplayName("-> 400 Bad Request")
|
||||||
void getDronesWithCooling_shouldReturn400_whenStateIsInvalid()
|
void getDronesWithCooling_shouldReturn400_whenStateIsInvalid() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String endpoint = API_ENDPOINT_BASE + "invalid";
|
String endpoint = API_ENDPOINT_BASE + "invalid";
|
||||||
mockMvc.perform(
|
mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON))
|
||||||
get(endpoint).contentType(MediaType.APPLICATION_JSON))
|
|
||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,33 +102,29 @@ public class DroneControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("-> 200 OK")
|
@DisplayName("-> 200 OK")
|
||||||
void getDroneDetails_shouldReturn200AndJson_whenCorrectInput()
|
void getDroneDetails_shouldReturn200AndJson_whenCorrectInput() throws Exception {
|
||||||
throws Exception {
|
Drone expected =
|
||||||
Drone expected = new Drone("Drone 1", "1",
|
new Drone(
|
||||||
new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f));
|
"Drone 1",
|
||||||
|
"1",
|
||||||
|
new DroneCapability(true, true, 4.0f, 2000, 0.01f, 4.3f, 6.5f));
|
||||||
String endpoint = API_ENDPOINT_BASE + "1";
|
String endpoint = API_ENDPOINT_BASE + "1";
|
||||||
when(
|
when(droneInfoService.droneDetail(anyString())).thenReturn(expected);
|
||||||
droneInfoService.droneDetail(anyString())).thenReturn(expected);
|
var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
||||||
var mock = mockMvc.perform(
|
|
||||||
get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
mock.andExpect(status().isOk());
|
mock.andExpect(status().isOk());
|
||||||
mock.andExpect(
|
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
content().json(objectMapper.writeValueAsString(expected)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("-> 404 Not Found")
|
@DisplayName("-> 404 Not Found")
|
||||||
void getDroneDetails_shouldReturn404_whenDroneNotFound()
|
void getDroneDetails_shouldReturn404_whenDroneNotFound() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String endpoint = API_ENDPOINT_BASE + "invalidDroneId";
|
String endpoint = API_ENDPOINT_BASE + "invalidDroneId";
|
||||||
when(
|
when(droneInfoService.droneDetail(anyString()))
|
||||||
droneInfoService.droneDetail(anyString())).thenThrow(new IllegalArgumentException());
|
.thenThrow(new IllegalArgumentException());
|
||||||
mockMvc.perform(
|
mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON))
|
||||||
get(endpoint).contentType(MediaType.APPLICATION_JSON))
|
|
||||||
.andExpect(status().isNotFound());
|
.andExpect(status().isNotFound());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
@ -148,64 +135,64 @@ public class DroneControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("capacity = 8 -> 200 OK")
|
@DisplayName("capacity = 8 -> 200 OK")
|
||||||
void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8()
|
void getQueryAsPath_shouldReturn200AndArrayOfString_whenCapacityIs8() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String attrName = "capacity";
|
String attrName = "capacity";
|
||||||
String attrVal = "8";
|
String attrVal = "8";
|
||||||
List<String> expected = List.of("2", "4", "7", "9");
|
List<String> expected = List.of("2", "4", "7", "9");
|
||||||
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("heating = true -> 200 OK")
|
@DisplayName("heating = true -> 200 OK")
|
||||||
void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue()
|
void getQueryAsPath_shouldReturn200AndArrayOfString_whenHeatingIsTrue() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String attrName = "heating";
|
String attrName = "heating";
|
||||||
String attrVal = "true";
|
String attrVal = "true";
|
||||||
List<String> expected = List.of("1", "2", "4", "5", "6", "7", "9");
|
List<String> expected = List.of("1", "2", "4", "5", "6", "7", "9");
|
||||||
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("cooling = false -> 200 OK")
|
@DisplayName("cooling = false -> 200 OK")
|
||||||
void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse()
|
void getQueryAsPath_shouldReturn200AndArrayOfString_whenCoolingIsFalse() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String attrName = "cooling";
|
String attrName = "cooling";
|
||||||
String attrVal = "false";
|
String attrVal = "false";
|
||||||
List<String> expected = List.of("2", "3", "4", "6", "7", "10");
|
List<String> expected = List.of("2", "3", "4", "6", "7", "10");
|
||||||
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("maxMoves = 1000 -> 200 OK")
|
@DisplayName("maxMoves = 1000 -> 200 OK")
|
||||||
void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000()
|
void getQueryAsPath_shouldReturn200AndArrayOfString_whenMaxMovesIs1000() throws Exception {
|
||||||
throws Exception {
|
|
||||||
String attrName = "maxMoves";
|
String attrName = "maxMoves";
|
||||||
String attrVal = "1000";
|
String attrVal = "1000";
|
||||||
List<String> expected = List.of("2", "4", "7", "9");
|
List<String> expected = List.of("2", "4", "7", "9");
|
||||||
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -220,8 +207,9 @@ public class DroneControllerTest {
|
||||||
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
when(droneAttrComparatorService.dronesWithAttribute(attrName, attrVal))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON))
|
get(API_ENDPOINT_BASE + attrName + "/" + attrVal)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -243,31 +231,34 @@ public class DroneControllerTest {
|
||||||
AttrQueryRequest[] requestBody = {req1, req2, req3};
|
AttrQueryRequest[] requestBody = {req1, req2, req3};
|
||||||
List<String> expected = List.of("8");
|
List<String> expected = List.of("8");
|
||||||
|
|
||||||
when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class)))
|
when(droneAttrComparatorService.dronesSatisfyingAttributes(
|
||||||
|
any(AttrQueryRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("GT LT -> 200 OK")
|
@DisplayName("GT LT -> 200 OK")
|
||||||
void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions()
|
void postQuery_shouldReturn200AndArrayOfString_whenGtLtConditions() throws Exception {
|
||||||
throws Exception {
|
|
||||||
AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
|
AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
|
||||||
AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
|
AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
|
||||||
AttrQueryRequest[] requestBody = {req1, req2};
|
AttrQueryRequest[] requestBody = {req1, req2};
|
||||||
List<String> expected = List.of("5", "10");
|
List<String> expected = List.of("5", "10");
|
||||||
|
|
||||||
when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class)))
|
when(droneAttrComparatorService.dronesSatisfyingAttributes(
|
||||||
|
any(AttrQueryRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -281,32 +272,35 @@ public class DroneControllerTest {
|
||||||
AttrQueryRequest[] requestBody = {req1, req2};
|
AttrQueryRequest[] requestBody = {req1, req2};
|
||||||
List<String> expected = List.of();
|
List<String> expected = List.of();
|
||||||
|
|
||||||
when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class)))
|
when(droneAttrComparatorService.dronesSatisfyingAttributes(
|
||||||
|
any(AttrQueryRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("GT LT EQ -> 200 OK")
|
@DisplayName("GT LT EQ -> 200 OK")
|
||||||
void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions()
|
void postQuery_shouldReturn200AndArrayOfString_whenGtLtEqConditions() throws Exception {
|
||||||
throws Exception {
|
|
||||||
AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
|
AttrQueryRequest req1 = new AttrQueryRequest("capacity", ">", "8");
|
||||||
AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
|
AttrQueryRequest req2 = new AttrQueryRequest("maxMoves", "<", "2000");
|
||||||
AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true");
|
AttrQueryRequest req3 = new AttrQueryRequest("cooling", "=", "true");
|
||||||
AttrQueryRequest[] requestBody = {req1, req2, req3};
|
AttrQueryRequest[] requestBody = {req1, req2, req3};
|
||||||
List<String> expected = List.of("5");
|
List<String> expected = List.of("5");
|
||||||
|
|
||||||
when(droneAttrComparatorService.dronesSatisfyingAttributes(any(AttrQueryRequest[].class)))
|
when(droneAttrComparatorService.dronesSatisfyingAttributes(
|
||||||
|
any(AttrQueryRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -324,16 +318,23 @@ public class DroneControllerTest {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
||||||
var delivery = new LngLat(-3.00, 55.121);
|
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};
|
MedDispatchRecRequest[] requestBody = {record};
|
||||||
List<String> expected = List.of("1", "2", "6", "7", "9");
|
List<String> expected = List.of("1", "2", "6", "7", "9");
|
||||||
|
|
||||||
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -342,24 +343,30 @@ public class DroneControllerTest {
|
||||||
@DisplayName("Treat Null as False (Cooling) -> 200 OK")
|
@DisplayName("Treat Null as False (Cooling) -> 200 OK")
|
||||||
void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenCoolingIsNull()
|
void postQueryAvailableDrones_shouldReturn200AndArrayOfString_whenCoolingIsNull()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
var requestMap = Map.of(
|
var requestMap =
|
||||||
"id", 123,
|
Map.of(
|
||||||
"date", "2025-12-22",
|
"id",
|
||||||
"time", "14:30",
|
123,
|
||||||
"requirements", Map.of(
|
"date",
|
||||||
"capacity", 0.75,
|
"2025-12-22",
|
||||||
"heating", true,
|
"time",
|
||||||
"maxCost", 13.5
|
"14:30",
|
||||||
)
|
"requirements",
|
||||||
);
|
Map.of(
|
||||||
|
"capacity", 0.75,
|
||||||
|
"heating", true,
|
||||||
|
"maxCost", 13.5));
|
||||||
List<String> expected = List.of("1", "2", "6", "7", "9");
|
List<String> expected = List.of("1", "2", "6", "7", "9");
|
||||||
|
|
||||||
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(new Object[]{requestMap})))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(
|
||||||
|
objectMapper.writeValueAsString(
|
||||||
|
new Object[] {requestMap})))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
|
|
@ -370,11 +377,23 @@ public class DroneControllerTest {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
var reqs1 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
var reqs1 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
||||||
var delivery1 = new LngLat(-3.00, 55.121);
|
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 reqs2 = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
||||||
var delivery2 = new LngLat(-3.00, 55.121);
|
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};
|
MedDispatchRecRequest[] requestBody = {record1, record2};
|
||||||
List<String> expected = List.of("2", "7", "9");
|
List<String> expected = List.of("2", "7", "9");
|
||||||
|
|
@ -382,11 +401,12 @@ public class DroneControllerTest {
|
||||||
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
when(droneInfoService.dronesMatchesRequirements(any(MedDispatchRecRequest[].class)))
|
||||||
.thenReturn(expected);
|
.thenReturn(expected);
|
||||||
|
|
||||||
mockMvc.perform(post(API_ENDPOINT)
|
mockMvc.perform(
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
post(API_ENDPOINT)
|
||||||
.content(objectMapper.writeValueAsString(requestBody)))
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(requestBody)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.Drone;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
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;
|
||||||
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
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.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.web.client.RestTemplate;
|
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)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class DroneInfoServiceTest {
|
public class DroneInfoServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock private RestTemplate restTemplate;
|
||||||
private RestTemplate restTemplate;
|
|
||||||
|
|
||||||
private DroneInfoService droneInfoService;
|
private DroneInfoService droneInfoService;
|
||||||
|
|
||||||
|
|
@ -44,9 +45,9 @@ public class DroneInfoServiceTest {
|
||||||
|
|
||||||
private Drone[] getMockDrones() {
|
private Drone[] getMockDrones() {
|
||||||
return new Drone[] {
|
return new Drone[] {
|
||||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
|
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 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 3", "3", new DroneCapability(false, false, 30, 3000, 3, 3, 3))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,30 +135,46 @@ public class DroneInfoServiceTest {
|
||||||
@DisplayName("dronesMatchesRequirements(MedDispatchRecRequest[]) tests")
|
@DisplayName("dronesMatchesRequirements(MedDispatchRecRequest[]) tests")
|
||||||
class DronesMatchesRequirementsTests {
|
class DronesMatchesRequirementsTests {
|
||||||
private ServicePointDrones[] getMockServicePointDrones() {
|
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);
|
DroneAvailability drone1Avail = new DroneAvailability("1", timeWindows);
|
||||||
ServicePointDrones spd = new ServicePointDrones(1, new DroneAvailability[] { drone1Avail });
|
ServicePointDrones spd =
|
||||||
return new ServicePointDrones[] { spd };
|
new ServicePointDrones(1, new DroneAvailability[] {drone1Avail});
|
||||||
|
return new ServicePointDrones[] {spd};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should return drones matching a single requirement")
|
@DisplayName("Should return drones matching a single requirement")
|
||||||
void dronesMatchesRequirements_shouldReturnMatchingDrones_forSingleRequirement() {
|
void dronesMatchesRequirements_shouldReturnMatchingDrones_forSingleRequirement() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var drones = new Drone[] {
|
var drones =
|
||||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
|
new Drone[] {
|
||||||
new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2))
|
new Drone(
|
||||||
};
|
"Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
|
||||||
when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones);
|
new Drone(
|
||||||
when(restTemplate.getForObject(URI.create(baseUrl + "drones-for-service-points"),
|
"Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2))
|
||||||
ServicePointDrones[].class)).thenReturn(getMockServicePointDrones());
|
};
|
||||||
|
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 requirement = new MedDispatchRecRequest.MedRequirement(8, true, false, 100);
|
||||||
var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0),
|
var record =
|
||||||
requirement, new LngLat(0, 0));
|
new MedDispatchRecRequest(
|
||||||
|
1,
|
||||||
|
LocalDate.now().with(DayOfWeek.MONDAY),
|
||||||
|
LocalTime.of(10, 0),
|
||||||
|
requirement,
|
||||||
|
new LngLat(0, 0));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
List<String> result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record });
|
List<String> result =
|
||||||
|
droneInfoService.dronesMatchesRequirements(
|
||||||
|
new MedDispatchRecRequest[] {record});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).containsExactly("1");
|
assertThat(result).containsExactly("1");
|
||||||
|
|
@ -167,19 +184,30 @@ public class DroneInfoServiceTest {
|
||||||
@DisplayName("Should return empty list if no drones match")
|
@DisplayName("Should return empty list if no drones match")
|
||||||
void dronesMatchesRequirements_shouldReturnEmptyList_whenNoDronesMatch() {
|
void dronesMatchesRequirements_shouldReturnEmptyList_whenNoDronesMatch() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var drones = new Drone[] {
|
var drones =
|
||||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)),
|
new Drone[] {
|
||||||
new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 2000, 2, 2, 2))
|
new Drone(
|
||||||
};
|
"Drone 1", "1", new DroneCapability(true, true, 5, 1000, 1, 1, 1)),
|
||||||
when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class)).thenReturn(drones);
|
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
|
// No need to mock drones-for-service-points as it won't be called
|
||||||
|
|
||||||
var requirement = new MedDispatchRecRequest.MedRequirement(10, true, false, 100);
|
var requirement = new MedDispatchRecRequest.MedRequirement(10, true, false, 100);
|
||||||
var record = new MedDispatchRecRequest(1, LocalDate.now().with(DayOfWeek.MONDAY), LocalTime.of(10, 0),
|
var record =
|
||||||
requirement, new LngLat(0, 0));
|
new MedDispatchRecRequest(
|
||||||
|
1,
|
||||||
|
LocalDate.now().with(DayOfWeek.MONDAY),
|
||||||
|
LocalTime.of(10, 0),
|
||||||
|
requirement,
|
||||||
|
new LngLat(0, 0));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
List<String> result = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[] { record });
|
List<String> result =
|
||||||
|
droneInfoService.dronesMatchesRequirements(
|
||||||
|
new MedDispatchRecRequest[] {record});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).isEmpty();
|
assertThat(result).isEmpty();
|
||||||
|
|
@ -194,7 +222,8 @@ public class DroneInfoServiceTest {
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
List<String> resultNull = droneInfoService.dronesMatchesRequirements(null);
|
List<String> resultNull = droneInfoService.dronesMatchesRequirements(null);
|
||||||
List<String> resultEmpty = droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]);
|
List<String> resultEmpty =
|
||||||
|
droneInfoService.dronesMatchesRequirements(new MedDispatchRecRequest[0]);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(resultNull).containsExactly("1", "2", "3");
|
assertThat(resultNull).containsExactly("1", "2", "3");
|
||||||
|
|
|
||||||
|
|
@ -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.Angle;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
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.common.Region;
|
||||||
import java.util.List;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class GpsCalculationServiceTest {
|
public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
private static final double STEP = 0.00015;
|
private static final double STEP = 0.00015;
|
||||||
|
|
@ -114,9 +116,7 @@ public class GpsCalculationServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName("True: Two points are close to each other and near threshold")
|
||||||
"True: Two points are close to each other and near threshold"
|
|
||||||
)
|
|
||||||
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
|
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
|
||||||
var p1 = new LngLat(0.0, 0.0);
|
var p1 = new LngLat(0.0, 0.0);
|
||||||
var p2 = new LngLat(0.0, 0.00014);
|
var p2 = new LngLat(0.0, 0.00014);
|
||||||
|
|
@ -160,20 +160,12 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
expected.lng(),
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
|
||||||
"Cardinal Direction: nextPosition in North direction (90 degrees)"
|
|
||||||
)
|
|
||||||
void nextPosition_shouldMoveNorth_forAngle90() {
|
void nextPosition_shouldMoveNorth_forAngle90() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
Angle angle = new Angle(90);
|
Angle angle = new Angle(90);
|
||||||
|
|
@ -182,20 +174,12 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
expected.lng(),
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
|
||||||
"Cardinal Direction: nextPosition in West direction (180 degrees)"
|
|
||||||
)
|
|
||||||
void nextPosition_shouldMoveWest_forAngle180() {
|
void nextPosition_shouldMoveWest_forAngle180() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
Angle angle = new Angle(180);
|
Angle angle = new Angle(180);
|
||||||
|
|
@ -205,20 +189,12 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
expected.lng(),
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
|
||||||
"Cardinal Direction: nextPosition in South direction (270 degrees)"
|
|
||||||
)
|
|
||||||
void nextPosition_shouldMoveSouth_forAngle270() {
|
void nextPosition_shouldMoveSouth_forAngle270() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
Angle angle = new Angle(270);
|
Angle angle = new Angle(270);
|
||||||
|
|
@ -228,20 +204,12 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
expected.lng(),
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName(
|
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
|
||||||
"Intercardinal Direction: nextPosition in Northeast direction (45 degrees)"
|
|
||||||
)
|
|
||||||
void nextPosition_shouldMoveNortheast_forAngle45() {
|
void nextPosition_shouldMoveNortheast_forAngle45() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
Angle angle = new Angle(45);
|
Angle angle = new Angle(45);
|
||||||
|
|
@ -252,47 +220,37 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
expected.lng(),
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName(
|
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
|
||||||
"Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean"
|
|
||||||
)
|
|
||||||
class CheckIsInRegionTests {
|
class CheckIsInRegionTests {
|
||||||
|
|
||||||
public static final Region RECTANGLE_REGION = new Region(
|
public static final Region RECTANGLE_REGION =
|
||||||
"rectangle",
|
new Region(
|
||||||
List.of(
|
"rectangle",
|
||||||
new LngLat(0.0, 0.0),
|
List.of(
|
||||||
new LngLat(2.0, 0.0),
|
new LngLat(0.0, 0.0),
|
||||||
new LngLat(2.0, 2.0),
|
new LngLat(2.0, 0.0),
|
||||||
new LngLat(0.0, 2.0),
|
new LngLat(2.0, 2.0),
|
||||||
new LngLat(0.0, 0.0)
|
new LngLat(0.0, 2.0),
|
||||||
)
|
new LngLat(0.0, 0.0)));
|
||||||
);
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("General Case: Given Example for Testing")
|
@DisplayName("General Case: Given Example for Testing")
|
||||||
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
|
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
|
||||||
var position = new LngLat(1.234, 1.222);
|
var position = new LngLat(1.234, 1.222);
|
||||||
var region = new Region(
|
var region =
|
||||||
"central",
|
new Region(
|
||||||
List.of(
|
"central",
|
||||||
new LngLat(-3.192473, 55.946233),
|
List.of(
|
||||||
new LngLat(-3.192473, 55.942617),
|
new LngLat(-3.192473, 55.946233),
|
||||||
new LngLat(-3.184319, 55.942617),
|
new LngLat(-3.192473, 55.942617),
|
||||||
new LngLat(-3.184319, 55.946233),
|
new LngLat(-3.184319, 55.942617),
|
||||||
new LngLat(-3.192473, 55.946233)
|
new LngLat(-3.184319, 55.946233),
|
||||||
)
|
new LngLat(-3.192473, 55.946233)));
|
||||||
);
|
|
||||||
boolean expected = false;
|
boolean expected = false;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -303,10 +261,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
|
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
|
||||||
var position = new LngLat(1.0, 1.0);
|
var position = new LngLat(1.0, 1.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,10 +270,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
|
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
|
||||||
var position = new LngLat(3.0, 1.0);
|
var position = new LngLat(3.0, 1.0);
|
||||||
boolean expected = false;
|
boolean expected = false;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,18 +278,17 @@ public class GpsCalculationServiceTest {
|
||||||
@DisplayName("General Case: Simple Hexagon")
|
@DisplayName("General Case: Simple Hexagon")
|
||||||
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
|
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
|
||||||
var position = new LngLat(2.0, 2.0);
|
var position = new LngLat(2.0, 2.0);
|
||||||
var region = new Region(
|
var region =
|
||||||
"hexagon",
|
new Region(
|
||||||
List.of(
|
"hexagon",
|
||||||
new LngLat(1.0, 0.0),
|
List.of(
|
||||||
new LngLat(4.0, 0.0),
|
new LngLat(1.0, 0.0),
|
||||||
new LngLat(5.0, 2.0),
|
new LngLat(4.0, 0.0),
|
||||||
new LngLat(4.0, 4.0),
|
new LngLat(5.0, 2.0),
|
||||||
new LngLat(1.0, 4.0),
|
new LngLat(4.0, 4.0),
|
||||||
new LngLat(0.0, 2.0),
|
new LngLat(1.0, 4.0),
|
||||||
new LngLat(1.0, 0.0)
|
new LngLat(0.0, 2.0),
|
||||||
)
|
new LngLat(1.0, 0.0)));
|
||||||
);
|
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -347,15 +298,14 @@ public class GpsCalculationServiceTest {
|
||||||
@DisplayName("Edge Case: Small Triangle")
|
@DisplayName("Edge Case: Small Triangle")
|
||||||
void isInRegion_shouldReturnTrue_forSmallTriangle() {
|
void isInRegion_shouldReturnTrue_forSmallTriangle() {
|
||||||
var position = new LngLat(0.00001, 0.00001);
|
var position = new LngLat(0.00001, 0.00001);
|
||||||
var region = new Region(
|
var region =
|
||||||
"triangle",
|
new Region(
|
||||||
List.of(
|
"triangle",
|
||||||
new LngLat(0.0, 0.0),
|
List.of(
|
||||||
new LngLat(0.0001, 0.0),
|
new LngLat(0.0, 0.0),
|
||||||
new LngLat(0.00005, 0.0001),
|
new LngLat(0.0001, 0.0),
|
||||||
new LngLat(0.0, 0.0)
|
new LngLat(0.00005, 0.0001),
|
||||||
)
|
new LngLat(0.0, 0.0)));
|
||||||
);
|
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -366,10 +316,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
|
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
|
||||||
var position = new LngLat(0.0, 1.0);
|
var position = new LngLat(0.0, 1.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,10 +325,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
|
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
|
||||||
var position = new LngLat(2.0, 1.0);
|
var position = new LngLat(2.0, 1.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,10 +334,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
|
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
|
||||||
var position = new LngLat(0.0, 1.0);
|
var position = new LngLat(0.0, 1.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,10 +343,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
|
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
|
||||||
var position = new LngLat(0.0, 0.0);
|
var position = new LngLat(0.0, 0.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,10 +352,7 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
|
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
|
||||||
var position = new LngLat(2.0, 2.0);
|
var position = new LngLat(2.0, 2.0);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
position,
|
|
||||||
RECTANGLE_REGION
|
|
||||||
);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,40 +360,40 @@ public class GpsCalculationServiceTest {
|
||||||
@DisplayName("Edge Case: Region not forming polygon")
|
@DisplayName("Edge Case: Region not forming polygon")
|
||||||
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
|
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
|
||||||
var position = new LngLat(2.0, 2.0);
|
var position = new LngLat(2.0, 2.0);
|
||||||
var region = new Region(
|
var region =
|
||||||
"line",
|
new Region(
|
||||||
List.of(
|
"line",
|
||||||
new LngLat(0.0, 0.0),
|
List.of(
|
||||||
new LngLat(0.0001, 0.0),
|
new LngLat(0.0, 0.0),
|
||||||
new LngLat(0.0, 0.0)
|
new LngLat(0.0001, 0.0),
|
||||||
)
|
new LngLat(0.0, 0.0)));
|
||||||
);
|
assertThatThrownBy(
|
||||||
assertThatThrownBy(() -> {
|
() -> {
|
||||||
service.checkIsInRegion(position, region);
|
service.checkIsInRegion(position, region);
|
||||||
})
|
})
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Region is not closed.");
|
.hasMessage("Region is not closed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Edge Case: Region is not closed")
|
@DisplayName("Edge Case: Region is not closed")
|
||||||
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
|
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
|
||||||
var position = new LngLat(2.0, 2.0);
|
var position = new LngLat(2.0, 2.0);
|
||||||
var region = new Region(
|
var region =
|
||||||
"rectangle",
|
new Region(
|
||||||
List.of(
|
"rectangle",
|
||||||
new LngLat(0.0, 0.0),
|
List.of(
|
||||||
new LngLat(2.0, 0.0),
|
new LngLat(0.0, 0.0),
|
||||||
new LngLat(2.0, 2.0),
|
new LngLat(2.0, 0.0),
|
||||||
new LngLat(0.0, 2.0),
|
new LngLat(2.0, 2.0),
|
||||||
new LngLat(0.0, -1.0)
|
new LngLat(0.0, 2.0),
|
||||||
)
|
new LngLat(0.0, -1.0)));
|
||||||
);
|
assertThatThrownBy(
|
||||||
assertThatThrownBy(() -> {
|
() -> {
|
||||||
service.checkIsInRegion(position, region);
|
service.checkIsInRegion(position, region);
|
||||||
})
|
})
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Region is not closed.");
|
.hasMessage("Region is not closed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -466,11 +401,12 @@ public class GpsCalculationServiceTest {
|
||||||
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
|
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
|
||||||
var position = new LngLat(2.0, 2.0);
|
var position = new LngLat(2.0, 2.0);
|
||||||
var region = new Region("rectangle", List.of());
|
var region = new Region("rectangle", List.of());
|
||||||
assertThatThrownBy(() -> {
|
assertThatThrownBy(
|
||||||
service.checkIsInRegion(position, region);
|
() -> {
|
||||||
})
|
service.checkIsInRegion(position, region);
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
})
|
||||||
.hasMessage("Region is not closed.");
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Region is not closed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
|
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.DroneCapability;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
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;
|
||||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest.MedRequirement;
|
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest.MedRequirement;
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
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.io.IOException;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class PathFinderServiceTest {
|
class PathFinderServiceTest {
|
||||||
|
|
@ -37,8 +40,7 @@ class PathFinderServiceTest {
|
||||||
private static final LngLat SERVICE_POINT_COORD = new LngLat(0.0, 0.0);
|
private static final LngLat SERVICE_POINT_COORD = new LngLat(0.0, 0.0);
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@Mock
|
@Mock private DroneInfoService droneInfoService;
|
||||||
private DroneInfoService droneInfoService;
|
|
||||||
|
|
||||||
private PathFinderService pathFinderService;
|
private PathFinderService pathFinderService;
|
||||||
|
|
||||||
|
|
@ -46,66 +48,38 @@ class PathFinderServiceTest {
|
||||||
void setUpPathFinder() {
|
void setUpPathFinder() {
|
||||||
GpsCalculationService gpsCalculationService = new GpsCalculationService();
|
GpsCalculationService gpsCalculationService = new GpsCalculationService();
|
||||||
|
|
||||||
DroneCapability capability = new DroneCapability(
|
DroneCapability capability = new DroneCapability(false, true, 5.0f, 10, 0.1f, 0.5f, 0.5f);
|
||||||
false,
|
|
||||||
true,
|
|
||||||
5.0f,
|
|
||||||
10,
|
|
||||||
0.1f,
|
|
||||||
0.5f,
|
|
||||||
0.5f
|
|
||||||
);
|
|
||||||
Drone drone = new Drone("Test Drone", DRONE_ID, capability);
|
Drone drone = new Drone("Test Drone", DRONE_ID, capability);
|
||||||
ServicePoint servicePoint = new ServicePoint(
|
ServicePoint servicePoint =
|
||||||
"Test Point",
|
new ServicePoint(
|
||||||
1,
|
"Test Point",
|
||||||
new LngLatAlt(
|
1,
|
||||||
SERVICE_POINT_COORD.lng(),
|
new LngLatAlt(SERVICE_POINT_COORD.lng(), SERVICE_POINT_COORD.lat(), 50.0));
|
||||||
SERVICE_POINT_COORD.lat(),
|
DroneAvailability availability =
|
||||||
50.0
|
new DroneAvailability(
|
||||||
)
|
DRONE_ID,
|
||||||
);
|
new TimeWindow[] {
|
||||||
DroneAvailability availability = new DroneAvailability(
|
new TimeWindow(DayOfWeek.MONDAY, LocalTime.MIDNIGHT, LocalTime.MAX),
|
||||||
DRONE_ID,
|
});
|
||||||
new TimeWindow[] {
|
ServicePointDrones servicePointDrones =
|
||||||
new TimeWindow(
|
new ServicePointDrones(servicePoint.id(), new DroneAvailability[] {availability});
|
||||||
DayOfWeek.MONDAY,
|
|
||||||
LocalTime.MIDNIGHT,
|
|
||||||
LocalTime.MAX
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
ServicePointDrones servicePointDrones = new ServicePointDrones(
|
|
||||||
servicePoint.id(),
|
|
||||||
new DroneAvailability[] { availability }
|
|
||||||
);
|
|
||||||
|
|
||||||
when(droneInfoService.fetchAllDrones()).thenReturn(List.of(drone));
|
when(droneInfoService.fetchAllDrones()).thenReturn(List.of(drone));
|
||||||
when(droneInfoService.fetchServicePoints()).thenReturn(
|
when(droneInfoService.fetchServicePoints()).thenReturn(List.of(servicePoint));
|
||||||
List.of(servicePoint)
|
when(droneInfoService.fetchDronesForServicePoints())
|
||||||
);
|
.thenReturn(List.of(servicePointDrones));
|
||||||
when(droneInfoService.fetchDronesForServicePoints()).thenReturn(
|
when(droneInfoService.fetchRestrictedAreas())
|
||||||
List.of(servicePointDrones)
|
.thenReturn(Collections.<RestrictedArea>emptyList());
|
||||||
);
|
when(droneInfoService.droneMatchesRequirement(any(), any())).thenReturn(true);
|
||||||
when(droneInfoService.fetchRestrictedAreas()).thenReturn(
|
pathFinderService = new PathFinderService(gpsCalculationService, droneInfoService);
|
||||||
Collections.<RestrictedArea>emptyList()
|
|
||||||
);
|
|
||||||
when(droneInfoService.droneMatchesRequirement(any(), any())).thenReturn(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
pathFinderService = new PathFinderService(
|
|
||||||
gpsCalculationService,
|
|
||||||
droneInfoService
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void calculateDeliveryPath_shouldStayWithinSingleTripBudget() {
|
void calculateDeliveryPath_shouldStayWithinSingleTripBudget() {
|
||||||
MedDispatchRecRequest request = createSampleRequest();
|
MedDispatchRecRequest request = createSampleRequest();
|
||||||
|
|
||||||
DeliveryPathResponse response = pathFinderService.calculateDeliveryPath(
|
DeliveryPathResponse response =
|
||||||
new MedDispatchRecRequest[] { request }
|
pathFinderService.calculateDeliveryPath(new MedDispatchRecRequest[] {request});
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(response.totalMoves()).isGreaterThan(0);
|
assertThat(response.totalMoves()).isGreaterThan(0);
|
||||||
assertThat(response.totalMoves()).isLessThanOrEqualTo(10);
|
assertThat(response.totalMoves()).isLessThanOrEqualTo(10);
|
||||||
|
|
@ -115,24 +89,18 @@ class PathFinderServiceTest {
|
||||||
assertThat(dronePath.deliveries()).hasSize(1);
|
assertThat(dronePath.deliveries()).hasSize(1);
|
||||||
var recordedPath = dronePath.deliveries().get(0).flightPath();
|
var recordedPath = dronePath.deliveries().get(0).flightPath();
|
||||||
assertThat(recordedPath.get(0)).isEqualTo(SERVICE_POINT_COORD);
|
assertThat(recordedPath.get(0)).isEqualTo(SERVICE_POINT_COORD);
|
||||||
assertThat(
|
assertThat(samePoint(recordedPath.get(recordedPath.size() - 1), SERVICE_POINT_COORD))
|
||||||
samePoint(
|
.isTrue();
|
||||||
recordedPath.get(recordedPath.size() - 1),
|
|
||||||
SERVICE_POINT_COORD
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.isTrue();
|
|
||||||
assertThat(hasHoverAt(recordedPath, request.delivery())).isTrue();
|
assertThat(hasHoverAt(recordedPath, request.delivery())).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void calculateDeliveryPathAsGeoJson_shouldReturnFeatureCollection()
|
void calculateDeliveryPathAsGeoJson_shouldReturnFeatureCollection() throws IOException {
|
||||||
throws IOException {
|
|
||||||
MedDispatchRecRequest request = createSampleRequest();
|
MedDispatchRecRequest request = createSampleRequest();
|
||||||
|
|
||||||
String geoJson = pathFinderService.calculateDeliveryPathAsGeoJson(
|
String geoJson =
|
||||||
new MedDispatchRecRequest[] { request }
|
pathFinderService.calculateDeliveryPathAsGeoJson(
|
||||||
);
|
new MedDispatchRecRequest[] {request});
|
||||||
|
|
||||||
JsonNode root = mapper.readTree(geoJson);
|
JsonNode root = mapper.readTree(geoJson);
|
||||||
assertThat(root.get("type").asText()).isEqualTo("FeatureCollection");
|
assertThat(root.get("type").asText()).isEqualTo("FeatureCollection");
|
||||||
|
|
@ -151,20 +119,16 @@ class PathFinderServiceTest {
|
||||||
|
|
||||||
double startLng = coordinates.get(0).get(0).asDouble();
|
double startLng = coordinates.get(0).get(0).asDouble();
|
||||||
double startLat = coordinates.get(0).get(1).asDouble();
|
double startLat = coordinates.get(0).get(1).asDouble();
|
||||||
assertThat(
|
assertThat(samePoint(new LngLat(startLng, startLat), SERVICE_POINT_COORD)).isTrue();
|
||||||
samePoint(new LngLat(startLng, startLat), SERVICE_POINT_COORD)
|
|
||||||
)
|
|
||||||
.isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MedDispatchRecRequest createSampleRequest() {
|
private MedDispatchRecRequest createSampleRequest() {
|
||||||
return new MedDispatchRecRequest(
|
return new MedDispatchRecRequest(
|
||||||
101,
|
101,
|
||||||
LocalDate.of(2025, 1, 6),
|
LocalDate.of(2025, 1, 6),
|
||||||
LocalTime.of(12, 0),
|
LocalTime.of(12, 0),
|
||||||
new MedRequirement(0.5f, false, true, 50.0f),
|
new MedRequirement(0.5f, false, true, 50.0f),
|
||||||
new LngLat(SERVICE_POINT_COORD.lng() + 0.0003, SERVICE_POINT_COORD.lat())
|
new LngLat(SERVICE_POINT_COORD.lng() + 0.0003, SERVICE_POINT_COORD.lat()));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasHoverAt(List<LngLat> path, LngLat target) {
|
private boolean hasHoverAt(List<LngLat> path, LngLat target) {
|
||||||
|
|
@ -178,9 +142,6 @@ class PathFinderServiceTest {
|
||||||
|
|
||||||
private boolean samePoint(LngLat a, LngLat b) {
|
private boolean samePoint(LngLat a, LngLat b) {
|
||||||
double threshold = 1e-9;
|
double threshold = 1e-9;
|
||||||
return (
|
return (Math.abs(a.lng() - b.lng()) < threshold && Math.abs(a.lat() - b.lat()) < threshold);
|
||||||
Math.abs(a.lng() - b.lng()) < threshold &&
|
|
||||||
Math.abs(a.lat() - b.lat()) < threshold
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue