fix(cw1): fix sematic erros
This commit is contained in:
parent
141a957a8d
commit
6795612079
12 changed files with 435 additions and 394 deletions
|
|
@ -43,6 +43,10 @@ body:json {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: eq 200
|
||||||
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
timeout: 0
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ body:json {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: eq 200
|
||||||
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
timeout: 0
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ body:json {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: eq 200
|
||||||
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
timeout: 0
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
|
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 io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||||
|
|
@ -79,7 +80,7 @@ public class ApiController {
|
||||||
@PostMapping("/nextPosition")
|
@PostMapping("/nextPosition")
|
||||||
public LngLat getNextPosition(@RequestBody MovementRequest request) {
|
public LngLat getNextPosition(@RequestBody MovementRequest request) {
|
||||||
LngLat start = request.start();
|
LngLat start = request.start();
|
||||||
double angle = request.angle();
|
Angle angle = new Angle(request.angle());
|
||||||
return gpsService.nextPosition(start, angle);
|
return gpsService.nextPosition(start, angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
|
public class GeoJsonDataController {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the data transfer object for angle
|
||||||
|
*
|
||||||
|
* @param val value of the angle in degrees
|
||||||
|
*/
|
||||||
|
public record Angle(double degrees) {
|
||||||
|
private static final double STEP = 22.5;
|
||||||
|
private static final double EPSILON = 1e-10;
|
||||||
|
|
||||||
|
public Angle {
|
||||||
|
if (degrees < 0 || degrees >= 360) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Angle must be in range [0, 360). Got: " + degrees
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be a multiple of 22.5 (one of the 16 major directions)
|
||||||
|
double remainder = degrees % STEP;
|
||||||
|
|
||||||
|
// Floating point modulo may have tiny errors, e.g. 45.0 % 22.5 could be 0.0 or 1.0e-15
|
||||||
|
// So we need to check if the remainder is small enough, or close enough to STEP (handling negative errors)
|
||||||
|
if (
|
||||||
|
Math.abs(remainder) > EPSILON &&
|
||||||
|
Math.abs(remainder - STEP) > EPSILON
|
||||||
|
) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Angle must be a multiple of 22.5 (one of the 16 major directions). Got: " +
|
||||||
|
degrees
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Angle fromIndex(int index) {
|
||||||
|
if (index < 0 || index > 15) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Direction index must be between 0 and 15"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Angle(index * STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double toRadians(double degrees) {
|
||||||
|
return Math.toRadians(degrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double toRadians() {
|
||||||
|
return Math.toRadians(degrees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,20 @@ package io.github.js0ny.ilp_coursework.data.common;
|
||||||
* @param lat latitude of the coordinate/point
|
* @param lat latitude of the coordinate/point
|
||||||
*/
|
*/
|
||||||
public record LngLat(double lng, double lat) {
|
public record LngLat(double lng, double lat) {
|
||||||
|
public LngLat {
|
||||||
|
if (lat < -90 || lat > 90) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Latitude must be between -90 and +90 degrees. Got: " + lat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lng < -180 || lng > 180) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Longitude must be between -180 and +180 degrees. Got: " + lng
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public LngLat(LngLatAlt coord) {
|
public LngLat(LngLatAlt coord) {
|
||||||
this(coord.lng(), coord.lat());
|
this(coord.lng(), coord.lat());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a region definition
|
* Represents the data transfer object for a region definition
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -48,4 +51,33 @@ public record Region(String name, List<LngLat> vertices) {
|
||||||
LngLat last = vertices.getLast();
|
LngLat last = vertices.getLast();
|
||||||
return Objects.equals(last, first);
|
return Objects.equals(last, first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> toGeoJson() {
|
||||||
|
try {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
List<List<Double>> ring = vertices.stream()
|
||||||
|
.map(v -> List.of(v.lng(), v.lat()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"type", "Polygon",
|
||||||
|
"coordinates", List.of(ring));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to generate GeoJSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toGeoJsonString() {
|
||||||
|
try {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
var geoJson = toGeoJson();
|
||||||
|
|
||||||
|
return mapper.writeValueAsString(geoJson);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to generate GeoJSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|
||||||
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.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.external.Drone;
|
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
import io.github.js0ny.ilp_coursework.data.external.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.AttrQueryRequest;
|
|
||||||
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.response.DeliveryPathResponse;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
|
|
||||||
import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.DayOfWeek;
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
@ -28,7 +23,6 @@ import org.springframework.web.client.RestTemplate;
|
||||||
public class DroneInfoService {
|
public class DroneInfoService {
|
||||||
|
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
private final String dronesEndpoint = "drones";
|
|
||||||
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";
|
||||||
|
|
@ -96,10 +90,6 @@ public class DroneInfoService {
|
||||||
public Drone droneDetail(String id) {
|
public Drone droneDetail(String id) {
|
||||||
List<Drone> drones = fetchAllDrones();
|
List<Drone> drones = fetchAllDrones();
|
||||||
|
|
||||||
if (drones == null) {
|
|
||||||
throw new NullPointerException("drone cannot be found");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var drone : drones) {
|
for (var drone : drones) {
|
||||||
if (drone.id().equals(id)) {
|
if (drone.id().equals(id)) {
|
||||||
return drone;
|
return drone;
|
||||||
|
|
@ -119,16 +109,12 @@ public class DroneInfoService {
|
||||||
* Associated service method with
|
* Associated service method with
|
||||||
*
|
*
|
||||||
* @param rec array of medical dispatch records
|
* @param rec array of medical dispatch records
|
||||||
* @return array of drone ids that match all the requirements
|
* @return List of drone ids that match all the requirements
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
|
||||||
*/
|
*/
|
||||||
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
||||||
List<Drone> drones = fetchAllDrones();
|
List<Drone> drones = fetchAllDrones();
|
||||||
|
|
||||||
if (drones == null) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rec == null || rec.length == 0) {
|
if (rec == null || rec.length == 0) {
|
||||||
return drones
|
return drones
|
||||||
.stream()
|
.stream()
|
||||||
|
|
@ -146,7 +132,7 @@ public class DroneInfoService {
|
||||||
.filter(d ->
|
.filter(d ->
|
||||||
Arrays.stream(rec)
|
Arrays.stream(rec)
|
||||||
.filter(r -> r != null && r.requirements() != null)
|
.filter(r -> r != null && r.requirements() != null)
|
||||||
.allMatch(r -> meetsRequirement(d, r))
|
.allMatch(r -> droneMatchesRequirement(d, r))
|
||||||
)
|
)
|
||||||
.map(Drone::id)
|
.map(Drone::id)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
@ -162,7 +148,10 @@ public class DroneInfoService {
|
||||||
* is invalid (capacity and id cannot be null
|
* is invalid (capacity and id cannot be null
|
||||||
* in {@code MedDispathRecDto})
|
* in {@code MedDispathRecDto})
|
||||||
*/
|
*/
|
||||||
public boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
|
public boolean droneMatchesRequirement(
|
||||||
|
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");
|
||||||
|
|
@ -184,7 +173,8 @@ public class DroneInfoService {
|
||||||
boolean requiredHeating = requirements.heating();
|
boolean requiredHeating = requirements.heating();
|
||||||
|
|
||||||
// Case 1: required is null: We don't care about it
|
// Case 1: required is null: We don't care about it
|
||||||
// Case 2: required is false: We don't care about it (high capability adapts to low requirements)
|
// Case 2: required is false: We don't care about it (high capability adapts to
|
||||||
|
// low requirements)
|
||||||
// Case 3: capability is true: Then always matches
|
// Case 3: capability is true: Then always matches
|
||||||
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
|
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
|
||||||
boolean matchesCooling = !requiredCooling || capability.cooling();
|
boolean matchesCooling = !requiredCooling || capability.cooling();
|
||||||
|
|
@ -197,7 +187,8 @@ public class DroneInfoService {
|
||||||
matchesHeating &&
|
matchesHeating &&
|
||||||
checkAvailability(drone.id(), record)
|
checkAvailability(drone.id(), record)
|
||||||
); // &&
|
); // &&
|
||||||
// checkCost(drone, record) // checkCost is more expensive than checkAvailability
|
// checkCost(drone, record) // checkCost is more expensive than
|
||||||
|
// checkAvailability
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -276,72 +267,15 @@ public class DroneInfoService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private Set<LngLat> parseObstacles() {
|
public List<Drone> fetchAllDrones() {
|
||||||
// URI restrictedAreasUrl = URI.create(baseUrl).resolve(
|
String dronesEndpoint = "drones";
|
||||||
// restrictedAreasEndpoint
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// RestrictedArea[] restrictedAreas = restTemplate.getForObject(
|
|
||||||
// restrictedAreasUrl,
|
|
||||||
// RestrictedArea[].class
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// assert restrictedAreas != null;
|
|
||||||
// Set<LngLat> obstacles = new HashSet<>();
|
|
||||||
// for (var ra : restrictedAreas) {
|
|
||||||
// obstacles.add(new LngLat(ra.location()));
|
|
||||||
// }
|
|
||||||
// return obstacles;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public DeliveryPathResponse calcDeliveryPath(
|
|
||||||
// MedDispatchRecRequest[] records
|
|
||||||
// ) {
|
|
||||||
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
|
||||||
// Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
|
||||||
// List<RestrictedArea> restrictedAreas = fetchRestrictedAreas();
|
|
||||||
// List<LngLat> totalPath = new ArrayList<>();
|
|
||||||
// List<Delivery> deliveries = new ArrayList<>();
|
|
||||||
|
|
||||||
// int moves = 0;
|
|
||||||
// float cost = 0;
|
|
||||||
// for (var record : records) {
|
|
||||||
// assert drones != null;
|
|
||||||
// Drone[] possibleDrones = Arrays.stream(drones)
|
|
||||||
// .filter(d -> meetsRequirement(d, record))
|
|
||||||
// .toArray(Drone[]::new);
|
|
||||||
// int shortestPathCount = Integer.MAX_VALUE;
|
|
||||||
// float lowestCost = Float.MAX_VALUE;
|
|
||||||
// List<LngLat> shortestPath = null;
|
|
||||||
// for (var d : possibleDrones) {
|
|
||||||
// var start = queryServicePointLocationByDroneId(d.id());
|
|
||||||
// List<LngLat> path = PathFinderService.findPath(
|
|
||||||
// start,
|
|
||||||
// record.delivery(),
|
|
||||||
// restrictedAreas
|
|
||||||
// );
|
|
||||||
// float pathCost = path.size() * d.capability().costPerMove();
|
|
||||||
// if (
|
|
||||||
// path.size() < d.capability().maxMoves() &&
|
|
||||||
// pathCost < lowestCost
|
|
||||||
// ) {
|
|
||||||
// shortestPathCount = path.size();
|
|
||||||
// lowestCost = pathCost;
|
|
||||||
// shortestPath = path;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // deliveries.add(new Delivery(record.id(), shortestPath));
|
|
||||||
// }
|
|
||||||
// // return new
|
|
||||||
// }
|
|
||||||
|
|
||||||
private List<Drone> fetchAllDrones() {
|
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||||
|
assert drones != null;
|
||||||
return Arrays.asList(drones);
|
return Arrays.asList(drones);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RestrictedArea> fetchRestrictedAreas() {
|
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||||
URI restrictedUrl = URI.create(baseUrl).resolve(
|
URI restrictedUrl = URI.create(baseUrl).resolve(
|
||||||
restrictedAreasEndpoint
|
restrictedAreasEndpoint
|
||||||
);
|
);
|
||||||
|
|
@ -350,13 +284,22 @@ public class DroneInfoService {
|
||||||
RestrictedArea[].class
|
RestrictedArea[].class
|
||||||
);
|
);
|
||||||
assert restrictedAreas != null;
|
assert restrictedAreas != null;
|
||||||
List<RestrictedArea> restrictedAreaList = Arrays.asList(
|
return Arrays.asList(restrictedAreas);
|
||||||
restrictedAreas
|
|
||||||
);
|
|
||||||
return restrictedAreaList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ServicePoint> fetchServicePoints() {
|
public List<String> fetchRestrictedAreasInGeoJson()
|
||||||
|
throws JsonProcessingException {
|
||||||
|
var mapper = new ObjectMapper();
|
||||||
|
var ras = fetchRestrictedAreas();
|
||||||
|
var geoJson = ras
|
||||||
|
.stream()
|
||||||
|
.map(RestrictedArea::toRegion)
|
||||||
|
.map(Region::toGeoJson)
|
||||||
|
.toList();
|
||||||
|
return Collections.singletonList(mapper.writeValueAsString(geoJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ServicePoint> fetchServicePoints() {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(
|
URI servicePointUrl = URI.create(baseUrl).resolve(
|
||||||
servicePointsEndpoint
|
servicePointsEndpoint
|
||||||
);
|
);
|
||||||
|
|
@ -365,11 +308,10 @@ public class DroneInfoService {
|
||||||
ServicePoint[].class
|
ServicePoint[].class
|
||||||
);
|
);
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
List<ServicePoint> servicePointList = Arrays.asList(servicePoints);
|
return Arrays.asList(servicePoints);
|
||||||
return servicePointList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ServicePointDrones> fetchDronesForServicePoints() {
|
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||||
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
|
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
|
||||||
dronesForServicePointsEndpoint
|
dronesForServicePointsEndpoint
|
||||||
);
|
);
|
||||||
|
|
@ -378,43 +320,6 @@ public class DroneInfoService {
|
||||||
ServicePointDrones[].class
|
ServicePointDrones[].class
|
||||||
);
|
);
|
||||||
assert servicePointDrones != null;
|
assert servicePointDrones != null;
|
||||||
List<ServicePointDrones> servicePointDronesList = Arrays.asList(
|
return Arrays.asList(servicePointDrones);
|
||||||
servicePointDrones
|
|
||||||
);
|
|
||||||
return servicePointDronesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: Not used.
|
|
||||||
private boolean checkCost(Drone drone, MedDispatchRecRequest rec) {
|
|
||||||
if (rec.delivery() == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(
|
|
||||||
dronesForServicePointsEndpoint
|
|
||||||
);
|
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
ServicePointDrones[].class
|
|
||||||
);
|
|
||||||
|
|
||||||
GpsCalculationService gpsService = new GpsCalculationService();
|
|
||||||
|
|
||||||
double steps = gpsService.calculateSteps(
|
|
||||||
queryServicePointLocationByDroneId(drone.id()),
|
|
||||||
rec.delivery()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (steps > drone.capability().maxMoves()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
float baseCost =
|
|
||||||
drone.capability().costInitial() + drone.capability().costFinal();
|
|
||||||
|
|
||||||
double cost = baseCost + drone.capability().costPerMove() * steps;
|
|
||||||
|
|
||||||
var requiredMaxCost = rec.requirements().maxCost();
|
|
||||||
|
|
||||||
return cost <= requiredMaxCost;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
|
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 io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||||
|
|
@ -84,8 +85,8 @@ public class GpsCalculationService {
|
||||||
* @see #STEP
|
* @see #STEP
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
||||||
*/
|
*/
|
||||||
public LngLat nextPosition(LngLat start, double angle) {
|
public LngLat nextPosition(LngLat start, Angle angle) {
|
||||||
double rad = Math.toRadians(angle); // Convert to radian for Java triangle function calculation
|
double rad = angle.toRadians();
|
||||||
double newLng = Math.cos(rad) * STEP + start.lng();
|
double newLng = Math.cos(rad) * STEP + start.lng();
|
||||||
double newLat = Math.sin(rad) * STEP + start.lat();
|
double newLat = Math.sin(rad) * STEP + start.lat();
|
||||||
return new LngLat(newLng, newLat);
|
return new LngLat(newLng, newLat);
|
||||||
|
|
|
||||||
|
|
@ -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.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.request.DistanceRequest;
|
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||||
|
|
@ -101,6 +102,25 @@ public class ApiControllerTest {
|
||||||
)
|
)
|
||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /distanceTo -> 400 Bad Request: Semantic errors")
|
||||||
|
void getDistance_shouldReturn400_whenInvalidInput() throws Exception {
|
||||||
|
String endpoint = "/api/v1/distanceTo";
|
||||||
|
String req = """
|
||||||
|
{ "position1": { "lng": -300.192473, "lat": 550.946233 }, "position2": { "lng": -3202.192473, "lat": 5533.942617 } }
|
||||||
|
""";
|
||||||
|
when(
|
||||||
|
service.calculateDistance(any(LngLat.class), isNull())
|
||||||
|
).thenThrow(new NullPointerException());
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post(endpoint)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(req)
|
||||||
|
)
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
@ -146,6 +166,25 @@ public class ApiControllerTest {
|
||||||
)
|
)
|
||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /isCloseTo -> 400 Bad Request: Semantic errors")
|
||||||
|
void getIsCloseTo_shouldReturn400_whenInvalidInput() throws Exception {
|
||||||
|
String endpoint = "/api/v1/isCloseTo";
|
||||||
|
String req = """
|
||||||
|
{ "position1": { "lng": -3004.192473, "lat": 550.946233 }, "position2": { "lng": -390.192473, "lat": 551.942617 } }
|
||||||
|
""";
|
||||||
|
when(
|
||||||
|
service.calculateDistance(any(LngLat.class), isNull())
|
||||||
|
).thenThrow(new NullPointerException());
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post(endpoint)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(req)
|
||||||
|
)
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
@ -162,7 +201,7 @@ public class ApiControllerTest {
|
||||||
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), anyDouble())
|
service.nextPosition(any(LngLat.class), any(Angle.class))
|
||||||
).thenReturn(expected);
|
).thenReturn(expected);
|
||||||
var mock = mockMvc.perform(
|
var mock = mockMvc.perform(
|
||||||
post(endpoint)
|
post(endpoint)
|
||||||
|
|
@ -187,7 +226,7 @@ public class ApiControllerTest {
|
||||||
"angle": 180
|
"angle": 180
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
when(service.nextPosition(isNull(), anyDouble())).thenThrow(
|
when(service.nextPosition(isNull(), any(Angle.class))).thenThrow(
|
||||||
new NullPointerException()
|
new NullPointerException()
|
||||||
);
|
);
|
||||||
mockMvc
|
mockMvc
|
||||||
|
|
@ -198,6 +237,32 @@ public class ApiControllerTest {
|
||||||
)
|
)
|
||||||
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /nextPosition -> 400 Bad Request: Semantic errors")
|
||||||
|
void getNextPosition_shouldReturn400_whenInvalidInput()
|
||||||
|
throws Exception {
|
||||||
|
String endpoint = "/api/v1/nextPosition";
|
||||||
|
String req = """
|
||||||
|
{
|
||||||
|
"start": {
|
||||||
|
"lng": -3.192473,
|
||||||
|
"lat": 55.946233
|
||||||
|
},
|
||||||
|
"angle": 900
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
when(
|
||||||
|
service.calculateDistance(any(LngLat.class), isNull())
|
||||||
|
).thenThrow(new NullPointerException());
|
||||||
|
mockMvc
|
||||||
|
.perform(
|
||||||
|
post(endpoint)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(req)
|
||||||
|
)
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.within;
|
import static org.assertj.core.api.AssertionsForClassTypes.within;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.Angle;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -52,7 +53,7 @@ public class GpsCalculationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Edge Case: Points are Identical")
|
@DisplayName("Edge Case: Points are Identical")
|
||||||
void calculateDistance_shouldReturnZero_whenPointsAreIdentical() {
|
void calculateDistance_shouldReturnZero_whenPointsAreIdentical() {
|
||||||
var p1 = new LngLat(123.85, 983.2119);
|
var p1 = new LngLat(12.85, 68.2119);
|
||||||
double expected = 0.0;
|
double expected = 0.0;
|
||||||
double actual = service.calculateDistance(p1, p1);
|
double actual = service.calculateDistance(p1, p1);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -61,8 +62,8 @@ public class GpsCalculationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Edge Case: Longitudinal-only movement")
|
@DisplayName("Edge Case: Longitudinal-only movement")
|
||||||
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() {
|
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() {
|
||||||
var p1 = new LngLat(123.85, 983.2119);
|
var p1 = new LngLat(23.85, 83.2119);
|
||||||
var p2 = new LngLat(133.85, 983.2119);
|
var p2 = new LngLat(33.85, 83.2119);
|
||||||
double expected = 10.0;
|
double expected = 10.0;
|
||||||
double actual = service.calculateDistance(p1, p2);
|
double actual = service.calculateDistance(p1, p2);
|
||||||
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
|
@ -71,8 +72,8 @@ public class GpsCalculationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Edge Case: Latitude-only movement")
|
@DisplayName("Edge Case: Latitude-only movement")
|
||||||
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() {
|
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() {
|
||||||
var p1 = new LngLat(123.85, 983.2119);
|
var p1 = new LngLat(123.85, 68.2119);
|
||||||
var p2 = new LngLat(123.85, 973.2119);
|
var p2 = new LngLat(123.85, 58.2119);
|
||||||
double expected = 10.0;
|
double expected = 10.0;
|
||||||
double actual = service.calculateDistance(p1, p2);
|
double actual = service.calculateDistance(p1, p2);
|
||||||
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
|
@ -106,7 +107,7 @@ public class GpsCalculationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("True: Two points are the same")
|
@DisplayName("True: Two points are the same")
|
||||||
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
|
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
|
||||||
var p1 = new LngLat(151.86, 285.37);
|
var p1 = new LngLat(15.86, 28.37);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.isCloseTo(p1, p1);
|
boolean actual = service.isCloseTo(p1, p1);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -153,7 +154,7 @@ public class GpsCalculationServiceTest {
|
||||||
@DisplayName("General Case: nextPosition in East direction (0 degrees)")
|
@DisplayName("General Case: nextPosition in East direction (0 degrees)")
|
||||||
void nextPosition_shouldMoveEast_forAngleZero() {
|
void nextPosition_shouldMoveEast_forAngleZero() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
double angle = 0;
|
Angle angle = new Angle(0);
|
||||||
// For 0 degrees, cos(0)=1, sin(0)=0. Move happens entirely on lng axis.
|
// For 0 degrees, cos(0)=1, sin(0)=0. Move happens entirely on lng axis.
|
||||||
var expected = new LngLat(STEP, 0.0);
|
var expected = new LngLat(STEP, 0.0);
|
||||||
|
|
||||||
|
|
@ -175,7 +176,7 @@ public class GpsCalculationServiceTest {
|
||||||
)
|
)
|
||||||
void nextPosition_shouldMoveNorth_forAngle90() {
|
void nextPosition_shouldMoveNorth_forAngle90() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
double angle = 90;
|
Angle angle = new Angle(90);
|
||||||
// For 90 degrees, cos(90)=0, sin(90)=1. Move happens entirely on lat axis.
|
// For 90 degrees, cos(90)=0, sin(90)=1. Move happens entirely on lat axis.
|
||||||
var expected = new LngLat(0.0, STEP);
|
var expected = new LngLat(0.0, STEP);
|
||||||
|
|
||||||
|
|
@ -197,7 +198,7 @@ public class GpsCalculationServiceTest {
|
||||||
)
|
)
|
||||||
void nextPosition_shouldMoveWest_forAngle180() {
|
void nextPosition_shouldMoveWest_forAngle180() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
double angle = 180;
|
Angle angle = new Angle(180);
|
||||||
// For 180 degrees, cos(180)=-1, sin(180)=0. Move happens entirely on negative
|
// For 180 degrees, cos(180)=-1, sin(180)=0. Move happens entirely on negative
|
||||||
// lng axis.
|
// lng axis.
|
||||||
var expected = new LngLat(-STEP, 0.0);
|
var expected = new LngLat(-STEP, 0.0);
|
||||||
|
|
@ -220,7 +221,7 @@ public class GpsCalculationServiceTest {
|
||||||
)
|
)
|
||||||
void nextPosition_shouldMoveSouth_forAngle270() {
|
void nextPosition_shouldMoveSouth_forAngle270() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
double angle = 270;
|
Angle angle = new Angle(270);
|
||||||
// For 270 degrees, cos(270)=0, sin(270)=-1. Move happens entirely on negative
|
// For 270 degrees, cos(270)=0, sin(270)=-1. Move happens entirely on negative
|
||||||
// lat axis.
|
// lat axis.
|
||||||
var expected = new LngLat(0.0, -STEP);
|
var expected = new LngLat(0.0, -STEP);
|
||||||
|
|
@ -243,10 +244,10 @@ public class GpsCalculationServiceTest {
|
||||||
)
|
)
|
||||||
void nextPosition_shouldMoveNortheast_forAngle45() {
|
void nextPosition_shouldMoveNortheast_forAngle45() {
|
||||||
var start = new LngLat(0.0, 0.0);
|
var start = new LngLat(0.0, 0.0);
|
||||||
double angle = 45;
|
Angle angle = new Angle(45);
|
||||||
// Δlng = step * cos(45°), Δlat = step * sin(45°)
|
// Δlng = step * cos(45°), Δlat = step * sin(45°)
|
||||||
double expectedLng = STEP * Math.cos(Math.toRadians(angle));
|
double expectedLng = STEP * Math.cos(angle.toRadians());
|
||||||
double expectedLat = STEP * Math.sin(Math.toRadians(angle));
|
double expectedLat = STEP * Math.sin(angle.toRadians());
|
||||||
var expected = new LngLat(expectedLng, expectedLat);
|
var expected = new LngLat(expectedLng, expectedLat);
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
@ -261,56 +262,10 @@ public class GpsCalculationServiceTest {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Edge Case: Angle larger than 360 should wrap around")
|
|
||||||
void nextPosition_shouldHandleAngleGreaterThan360() {
|
|
||||||
var start = new LngLat(0.0, 0.0);
|
|
||||||
// 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
|
|
||||||
double angle = 405;
|
|
||||||
double equivalentAngle = 45;
|
|
||||||
double expectedLng =
|
|
||||||
STEP * Math.cos(Math.toRadians(equivalentAngle));
|
|
||||||
double expectedLat =
|
|
||||||
STEP * Math.sin(Math.toRadians(equivalentAngle));
|
|
||||||
var expected = new LngLat(expectedLng, expectedLat);
|
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
|
||||||
expected.lng(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("Edge Case: Negative angle should work correctly")
|
|
||||||
void nextPosition_shouldHandleNegativeAngle() {
|
|
||||||
var start = new LngLat(0.0, 0.0);
|
|
||||||
// A negative angle of -45° corresponds to the Southeast direction.
|
|
||||||
double angle = -45;
|
|
||||||
double expectedLng = STEP * Math.cos(Math.toRadians(angle));
|
|
||||||
double expectedLat = STEP * Math.sin(Math.toRadians(angle));
|
|
||||||
var expected = new LngLat(expectedLng, expectedLat);
|
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(
|
|
||||||
expected.lng(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
assertThat(actual.lat()).isCloseTo(
|
|
||||||
expected.lat(),
|
|
||||||
within(PRECISION)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
|
@DisplayName(
|
||||||
|
"Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean"
|
||||||
|
)
|
||||||
class CheckIsInRegionTests {
|
class CheckIsInRegionTests {
|
||||||
|
|
||||||
public static final Region RECTANGLE_REGION = new Region(
|
public static final Region RECTANGLE_REGION = new Region(
|
||||||
|
|
@ -518,4 +473,5 @@ public class GpsCalculationServiceTest {
|
||||||
.hasMessage("Region is not closed.");
|
.hasMessage("Region is not closed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue