refractor(controller): Use List<> instead of []
This commit is contained in:
parent
69d9e0d736
commit
141a957a8d
24 changed files with 991 additions and 380 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -39,3 +39,4 @@ out/
|
||||||
*.tar
|
*.tar
|
||||||
.direnv/
|
.direnv/
|
||||||
.envrc
|
.envrc
|
||||||
|
localjson
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM --platform=linux/amd64 openjdk:21
|
FROM maven:3.9.9-amazoncorretto-21-debian AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ body:json {
|
||||||
"capacity": 0.75,
|
"capacity": 0.75,
|
||||||
"heating": true,
|
"heating": true,
|
||||||
"maxCost": 13.5
|
"maxCost": 13.5
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"lng": -3.00,
|
||||||
|
"lat": 55.121
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -30,6 +34,10 @@ body:json {
|
||||||
"capacity": 0.75,
|
"capacity": 0.75,
|
||||||
"heating": true,
|
"heating": true,
|
||||||
"maxCost": 13.5
|
"maxCost": 13.5
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"lng": -3.00,
|
||||||
|
"lat": 55.121
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ body:json {
|
||||||
"cooling": false,
|
"cooling": false,
|
||||||
"heating": true,
|
"heating": true,
|
||||||
"maxCost": 13.5
|
"maxCost": 13.5
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"lng": -3.00,
|
||||||
|
"lat": 55.121
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ body:json {
|
||||||
"capacity": 0.75,
|
"capacity": 0.75,
|
||||||
"heating": true,
|
"heating": true,
|
||||||
"maxCost": 13.5
|
"maxCost": 13.5
|
||||||
}
|
},
|
||||||
|
"delivery": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
|
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.request.DistanceRequest;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||||
|
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.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;
|
||||||
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 io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
|
||||||
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.common.Region;
|
|
||||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main REST Controller for the ILP Coursework 1 application.
|
* Main REST Controller for the ILP Coursework 1 application.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -53,7 +52,6 @@ public class ApiController {
|
||||||
*/
|
*/
|
||||||
@PostMapping("/distanceTo")
|
@PostMapping("/distanceTo")
|
||||||
public double getDistance(@RequestBody DistanceRequest request) {
|
public double getDistance(@RequestBody DistanceRequest request) {
|
||||||
|
|
||||||
LngLat position1 = request.position1();
|
LngLat position1 = request.position1();
|
||||||
LngLat position2 = request.position2();
|
LngLat position2 = request.position2();
|
||||||
return gpsService.calculateDistance(position1, position2);
|
return gpsService.calculateDistance(position1, position2);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
|
||||||
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.common.DronePathDto;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
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.response.DeliveryPathResponse;
|
||||||
|
import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
|
||||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||||
|
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 org.springframework.web.client.RestTemplate;
|
||||||
|
|
@ -21,8 +22,8 @@ import org.springframework.web.client.RestTemplate;
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
public class DroneController {
|
public class DroneController {
|
||||||
|
|
||||||
private final DroneInfoService droneService;
|
private final DroneInfoService droneInfoService;
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final DroneAttrComparatorService droneAttrComparatorService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of the {@code DroneController} with the business logic dependency
|
* Constructor of the {@code DroneController} with the business logic dependency
|
||||||
|
|
@ -34,8 +35,12 @@ public class DroneController {
|
||||||
*
|
*
|
||||||
* @param droneService The service component that contains all business logic
|
* @param droneService The service component that contains all business logic
|
||||||
*/
|
*/
|
||||||
public DroneController(DroneInfoService droneService) {
|
public DroneController(
|
||||||
this.droneService = droneService;
|
DroneInfoService droneService,
|
||||||
|
DroneAttrComparatorService droneAttrComparatorService
|
||||||
|
) {
|
||||||
|
this.droneInfoService = droneService;
|
||||||
|
this.droneAttrComparatorService = droneAttrComparatorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,8 +52,10 @@ public class DroneController {
|
||||||
* @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 String[] getDronesWithCoolingCapability(@PathVariable boolean state) {
|
public List<String> getDronesWithCoolingCapability(
|
||||||
return droneService.dronesWithCooling(state);
|
@PathVariable boolean state
|
||||||
|
) {
|
||||||
|
return droneInfoService.dronesWithCooling(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,7 +68,7 @@ public class DroneController {
|
||||||
@GetMapping("/droneDetails/{id}")
|
@GetMapping("/droneDetails/{id}")
|
||||||
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
|
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
|
||||||
try {
|
try {
|
||||||
Drone drone = droneService.droneDetail(id);
|
Drone drone = droneInfoService.droneDetail(id);
|
||||||
return ResponseEntity.ok(drone);
|
return ResponseEntity.ok(drone);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
|
|
@ -77,30 +84,44 @@ public class DroneController {
|
||||||
* @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 String[] getIdByAttrMap(
|
public List<String> getIdByAttrMap(
|
||||||
@PathVariable String attrName,
|
@PathVariable String attrName,
|
||||||
@PathVariable String attrVal) {
|
@PathVariable String attrVal
|
||||||
return droneService.dronesWithAttribute(attrName, attrVal);
|
) {
|
||||||
|
return droneAttrComparatorService.dronesWithAttribute(
|
||||||
|
attrName,
|
||||||
|
attrVal
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
|
public List<String> getIdByAttrMapPost(
|
||||||
return droneService.dronesSatisfyingAttributes(attrComparators);
|
@RequestBody AttrQueryRequest[] attrComparators
|
||||||
|
) {
|
||||||
|
return droneAttrComparatorService.dronesSatisfyingAttributes(
|
||||||
|
attrComparators
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/queryAvailableDrones")
|
@PostMapping("/queryAvailableDrones")
|
||||||
public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
|
public List<String> queryAvailableDrones(
|
||||||
return droneService.dronesMatchesRequirements(records);
|
@RequestBody MedDispatchRecRequest[] records
|
||||||
|
) {
|
||||||
|
return droneInfoService.dronesMatchesRequirements(records);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/calcDeliveryPath")
|
@PostMapping("/calcDeliveryPath")
|
||||||
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
|
public DeliveryPathResponse calculateDeliveryPath(
|
||||||
return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{});
|
@RequestBody MedDispatchRecRequest[] record
|
||||||
|
) {
|
||||||
|
// return new DeliveryPathResponse(0.0f, 0, new DronePathDto[] {});
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/calcDeliveryPathAsGeoJson")
|
@PostMapping("/calcDeliveryPathAsGeoJson")
|
||||||
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
|
public String calculateDeliveryPathAsGeoJson(
|
||||||
|
@RequestBody MedDispatchRecRequest[] record
|
||||||
|
) {
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a range of altitude values (that is a fly-zone in {@link RestrictedArea}).
|
||||||
|
*
|
||||||
|
* @param lower The lower bound of the altitude range.
|
||||||
|
* @param upper The upper bound of the altitude range. If {@code upper = -1}, then the region
|
||||||
|
* is not a fly zone.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public record AltitudeRange(double lower, double upper) {
|
||||||
|
}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
|
||||||
|
|
||||||
public record DronePathDto() {
|
|
||||||
}
|
|
||||||
|
|
@ -8,4 +8,7 @@ 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(LngLatAlt coord) {
|
||||||
|
this(coord.lng(), coord.lat());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the data transfer object for a point or coordinate
|
||||||
|
* that defines by a longitude and latitude
|
||||||
|
*
|
||||||
|
* @param lng longitude of the coordinate/point
|
||||||
|
* @param lat latitude of the coordinate/point
|
||||||
|
* @param alt altitude of the coordinate/point
|
||||||
|
*/
|
||||||
|
public record LngLatAlt(double lng, double lat, double alt) {
|
||||||
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
|
||||||
|
|
||||||
public record MedRequirement(
|
|
||||||
float capacity,
|
|
||||||
boolean cooling,
|
|
||||||
boolean heating,
|
|
||||||
float maxCost
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -47,5 +48,4 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
vendored
Normal file
23
src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data.external;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.AltitudeRange;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record RestrictedArea(
|
||||||
|
String name,
|
||||||
|
int id,
|
||||||
|
AltitudeRange limits,
|
||||||
|
LngLatAlt[] vertices
|
||||||
|
) {
|
||||||
|
public Region toRegion() {
|
||||||
|
List<LngLat> vertices2D = new ArrayList<>();
|
||||||
|
for (var vertex : vertices) {
|
||||||
|
vertices2D.add(new LngLat(vertex.lng(), vertex.lat()));
|
||||||
|
}
|
||||||
|
return new Region(name, vertices2D);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
vendored
Normal file
5
src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data.external;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
||||||
|
|
||||||
|
public record ServicePoint(String name, int id, LngLatAlt location) {}
|
||||||
|
|
@ -4,7 +4,7 @@ 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(
|
||||||
String servicePointId,
|
int servicePointId,
|
||||||
DroneAvailability[] drones) {
|
DroneAvailability[] drones) {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.request;
|
package io.github.js0ny.ilp_coursework.data.request;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.MedRequirement;
|
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;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record MedDispatchRecRequest(
|
public record MedDispatchRecRequest(
|
||||||
int id,
|
int id,
|
||||||
LocalDate date,
|
LocalDate date,
|
||||||
LocalTime time,
|
LocalTime time,
|
||||||
MedRequirement requirements,
|
MedRequirement requirements,
|
||||||
LngLat delivery) {
|
LngLat delivery) {
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record MedRequirement(
|
||||||
|
float capacity,
|
||||||
|
boolean cooling,
|
||||||
|
boolean heating,
|
||||||
|
float maxCost
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.response;
|
package io.github.js0ny.ilp_coursework.data.response;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public record DeliveryPathResponse(
|
public record DeliveryPathResponse(
|
||||||
float totalCost,
|
float totalCost,
|
||||||
int totalMoves,
|
int totalMoves,
|
||||||
DronePathDto[] dronePaths) {
|
DronePath[] dronePaths
|
||||||
|
) {
|
||||||
|
public record DronePath(int droneId, List<Delivery> deliveries) {
|
||||||
|
public record Delivery(int deliveryId, List<LngLat> flightPath) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
|
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
||||||
|
import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DroneAttrComparatorService {
|
||||||
|
|
||||||
|
private final String baseUrl;
|
||||||
|
private final String dronesEndpoint = "drones";
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor, handles the base url here.
|
||||||
|
*/
|
||||||
|
public DroneAttrComparatorService() {
|
||||||
|
String baseUrl = System.getenv("ILP_ENDPOINT");
|
||||||
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
|
this.baseUrl =
|
||||||
|
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
||||||
|
} else {
|
||||||
|
// Defensive: Add '/' to the end of the URL
|
||||||
|
if (!baseUrl.endsWith("/")) {
|
||||||
|
baseUrl += "/";
|
||||||
|
}
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of ids of drones with a given attribute name and value.
|
||||||
|
* <p>
|
||||||
|
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
|
||||||
|
*
|
||||||
|
* @param attrName the attribute name to filter on
|
||||||
|
* @param attrVal the attribute value to filter on
|
||||||
|
* @return array of drone ids matching the attribute name and value
|
||||||
|
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
|
||||||
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
|
||||||
|
*/
|
||||||
|
public List<String> dronesWithAttribute(String attrName, String attrVal) {
|
||||||
|
// Call the helper with EQ operator
|
||||||
|
return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of ids of drones which matches all given complex comparing
|
||||||
|
* rules
|
||||||
|
*
|
||||||
|
* @param attrComparators The filter rule with Name, Value and Operator
|
||||||
|
* @return array of drone ids that matches all rules
|
||||||
|
*/
|
||||||
|
public List<String> dronesSatisfyingAttributes(
|
||||||
|
AttrQueryRequest[] attrComparators
|
||||||
|
) {
|
||||||
|
Set<String> matchingDroneIds = null;
|
||||||
|
for (var comparator : attrComparators) {
|
||||||
|
String attribute = comparator.attribute();
|
||||||
|
String operator = comparator.operator();
|
||||||
|
String value = comparator.value();
|
||||||
|
AttrOperator op = AttrOperator.fromString(operator);
|
||||||
|
List<String> ids = dronesWithAttributeCompared(
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
op
|
||||||
|
);
|
||||||
|
if (matchingDroneIds == null) {
|
||||||
|
matchingDroneIds = new HashSet<>(ids);
|
||||||
|
} else {
|
||||||
|
matchingDroneIds.retainAll(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchingDroneIds == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return matchingDroneIds.stream().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that wraps the dynamic querying with different comparison operators
|
||||||
|
* <p>
|
||||||
|
* This method act as a concatenation of
|
||||||
|
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
||||||
|
* AttrOperator)}
|
||||||
|
*
|
||||||
|
* @param attrName the attribute name to filter on
|
||||||
|
* @param attrVal the attribute value to filter on
|
||||||
|
* @param op the comparison operator
|
||||||
|
* @return array of drone ids matching the attribute name and value (filtered by
|
||||||
|
* {@code op})
|
||||||
|
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
|
||||||
|
* String,
|
||||||
|
* AttrOperator)
|
||||||
|
*/
|
||||||
|
private List<String> dronesWithAttributeCompared(
|
||||||
|
String attrName,
|
||||||
|
String attrVal,
|
||||||
|
AttrOperator op
|
||||||
|
) {
|
||||||
|
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
|
// This is required to make sure the response is valid
|
||||||
|
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||||
|
|
||||||
|
if (drones == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
|
||||||
|
// querying
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
return Arrays.stream(drones)
|
||||||
|
.filter(drone -> {
|
||||||
|
JsonNode node = mapper.valueToTree(drone);
|
||||||
|
JsonNode attrNode = node.findValue(attrName);
|
||||||
|
if (attrNode != null) {
|
||||||
|
// Manually handle different types of JsonNode
|
||||||
|
return isValueMatched(attrNode, attrVal, op);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(Drone::id)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,38 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
|
||||||
import io.github.js0ny.ilp_coursework.util.AttrOperator;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
|
||||||
|
|
||||||
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
|
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.ServicePoint;
|
||||||
|
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.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;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Set;
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class DroneInfoService {
|
public class DroneInfoService {
|
||||||
|
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
private final String dronesEndpoint = "drones";
|
private final String dronesEndpoint = "drones";
|
||||||
private final String dronesForServicePointsEndpoint = "drones-for-service-points";
|
private final String dronesForServicePointsEndpoint =
|
||||||
|
"drones-for-service-points";
|
||||||
|
public static final String servicePointsEndpoint = "service-points";
|
||||||
|
public static final String restrictedAreasEndpoint = "restricted-areas";
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
|
@ -37,7 +42,8 @@ public class DroneInfoService {
|
||||||
public DroneInfoService() {
|
public DroneInfoService() {
|
||||||
String baseUrl = System.getenv("ILP_ENDPOINT");
|
String baseUrl = System.getenv("ILP_ENDPOINT");
|
||||||
if (baseUrl == null || baseUrl.isBlank()) {
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
this.baseUrl =
|
||||||
|
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
|
||||||
} else {
|
} else {
|
||||||
// Defensive: Add '/' to the end of the URL
|
// Defensive: Add '/' to the end of the URL
|
||||||
if (!baseUrl.endsWith("/")) {
|
if (!baseUrl.endsWith("/")) {
|
||||||
|
|
@ -57,20 +63,20 @@ public class DroneInfoService {
|
||||||
* capability, else without cooling
|
* capability, else without cooling
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||||
*/
|
*/
|
||||||
public String[] dronesWithCooling(boolean state) {
|
public List<String> dronesWithCooling(boolean state) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
Drone[] drones = restTemplate.getForObject(
|
// Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||||
droneUrl,
|
List<Drone> drones = fetchAllDrones();
|
||||||
Drone[].class);
|
|
||||||
|
|
||||||
if (drones == null) {
|
if (drones == null) {
|
||||||
return new String[]{};
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Arrays.stream(drones)
|
return drones
|
||||||
|
.stream()
|
||||||
.filter(drone -> drone.capability().cooling() == state)
|
.filter(drone -> drone.capability().cooling() == state)
|
||||||
.map(Drone::id)
|
.map(Drone::id)
|
||||||
.toArray(String[]::new);
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,10 +94,7 @@ public class DroneInfoService {
|
||||||
* @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) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
List<Drone> drones = fetchAllDrones();
|
||||||
Drone[] drones = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
Drone[].class);
|
|
||||||
|
|
||||||
if (drones == null) {
|
if (drones == null) {
|
||||||
throw new NullPointerException("drone cannot be found");
|
throw new NullPointerException("drone cannot be found");
|
||||||
|
|
@ -105,96 +108,8 @@ 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 with a given attribute name and value.
|
|
||||||
* <p>
|
|
||||||
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
|
|
||||||
*
|
|
||||||
* @param attrName the attribute name to filter on
|
|
||||||
* @param attrVal the attribute value to filter on
|
|
||||||
* @return array of drone ids matching the attribute name and value
|
|
||||||
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
|
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
|
|
||||||
*/
|
|
||||||
public String[] dronesWithAttribute(String attrName, String attrVal) {
|
|
||||||
// Call the helper with EQ operator
|
|
||||||
return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array of ids of drones which matches all given complex comparing
|
|
||||||
* rules
|
|
||||||
*
|
|
||||||
* @param attrComparators The filter rule with Name, Value and Operator
|
|
||||||
* @return array of drone ids that matches all rules
|
|
||||||
*/
|
|
||||||
public String[] dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
|
|
||||||
Set<String> matchingDroneIds = null;
|
|
||||||
for (var comparator : attrComparators) {
|
|
||||||
String attribute = comparator.attribute();
|
|
||||||
String operator = comparator.operator();
|
|
||||||
String value = comparator.value();
|
|
||||||
AttrOperator op = AttrOperator.fromString(operator);
|
|
||||||
String[] ids = dronesWithAttributeCompared(attribute, value, op);
|
|
||||||
if (matchingDroneIds == null) {
|
|
||||||
matchingDroneIds = new HashSet<>(Arrays.asList(ids));
|
|
||||||
} else {
|
|
||||||
matchingDroneIds.retainAll(Arrays.asList(ids));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matchingDroneIds == null) {
|
|
||||||
return new String[]{};
|
|
||||||
}
|
|
||||||
return matchingDroneIds.toArray(String[]::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper that wraps the dynamic querying with different comparison operators
|
|
||||||
* <p>
|
|
||||||
* This method act as a concatenation of
|
|
||||||
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
|
|
||||||
* AttrOperator)}
|
|
||||||
*
|
|
||||||
* @param attrName the attribute name to filter on
|
|
||||||
* @param attrVal the attribute value to filter on
|
|
||||||
* @param op the comparison operator
|
|
||||||
* @return array of drone ids matching the attribute name and value (filtered by
|
|
||||||
* {@code op})
|
|
||||||
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
|
|
||||||
* String,
|
|
||||||
* AttrOperator)
|
|
||||||
*/
|
|
||||||
private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) {
|
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
|
||||||
// This is required to make sure the response is valid
|
|
||||||
Drone[] drones = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
Drone[].class);
|
|
||||||
|
|
||||||
if (drones == null) {
|
|
||||||
return new String[]{};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
|
|
||||||
// querying
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
return Arrays.stream(drones)
|
|
||||||
.filter(drone -> {
|
|
||||||
JsonNode node = mapper.valueToTree(drone);
|
|
||||||
JsonNode attrNode = node.findValue(attrName);
|
|
||||||
if (attrNode != null) {
|
|
||||||
// Manually handle different types of JsonNode
|
|
||||||
return isValueMatched(attrNode, attrVal, op);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(Drone::id)
|
|
||||||
.toArray(String[]::new);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -207,27 +122,34 @@ public class DroneInfoService {
|
||||||
* @return array of drone ids that match all the requirements
|
* @return array 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 String[] dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
List<Drone> drones = fetchAllDrones();
|
||||||
Drone[] drones = restTemplate.getForObject(
|
|
||||||
droneUrl,
|
|
||||||
Drone[].class);
|
|
||||||
|
|
||||||
if (drones == null || rec == null || rec.length == 0) {
|
if (drones == null) {
|
||||||
return new String[]{};
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rec == null || rec.length == 0) {
|
||||||
|
return drones
|
||||||
|
.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(Drone::id)
|
||||||
|
.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 Arrays.stream(drones)
|
return drones
|
||||||
.filter(drone -> drone != null && drone.capability() != null)
|
.stream()
|
||||||
.filter(drone -> Arrays.stream(rec)
|
.filter(d -> d != null && d.capability() != null)
|
||||||
.filter(record -> record != null && record.requirements() != null)
|
.filter(d ->
|
||||||
// Every record must be met
|
Arrays.stream(rec)
|
||||||
.allMatch(record -> meetsRequirement(drone, record)))
|
.filter(r -> r != null && r.requirements() != null)
|
||||||
|
.allMatch(r -> meetsRequirement(d, r))
|
||||||
|
)
|
||||||
.map(Drone::id)
|
.map(Drone::id)
|
||||||
.toArray(String[]::new);
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -240,14 +162,16 @@ 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})
|
||||||
*/
|
*/
|
||||||
private boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
|
public boolean meetsRequirement(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("drone capability cannot be null");
|
throw new IllegalArgumentException(
|
||||||
|
"drone capability cannot be null"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
float requiredCapacity = requirements.capacity();
|
float requiredCapacity = requirements.capacity();
|
||||||
|
|
@ -256,24 +180,24 @@ public class DroneInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use boolean wrapper to allow null (not specified) values
|
// Use boolean wrapper to allow null (not specified) values
|
||||||
Boolean requiredCooling = requirements.cooling();
|
boolean requiredCooling = requirements.cooling();
|
||||||
Boolean requiredHeating = requirements.heating();
|
boolean requiredHeating = requirements.heating();
|
||||||
Float requiredMaxCost = requirements.maxCost();
|
|
||||||
|
|
||||||
boolean matchesCooling = requiredCooling == null || capability.cooling() == requiredCooling;
|
// Case 1: required is null: We don't care about it
|
||||||
boolean matchesHeating = requiredHeating == null || capability.heating() == requiredHeating;
|
// Case 2: required is false: We don't care about it (high capability adapts to low requirements)
|
||||||
boolean matchesCost = false;
|
// Case 3: capability is true: Then always matches
|
||||||
|
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
|
||||||
float totalCost = capability.costInitial() + capability.costFinal();
|
boolean matchesCooling = !requiredCooling || capability.cooling();
|
||||||
|
boolean matchesHeating = !requiredHeating || capability.heating();
|
||||||
if (capability.maxMoves() > 0) {
|
|
||||||
totalCost += capability.costPerMove();
|
|
||||||
}
|
|
||||||
matchesCost = totalCost <= requiredMaxCost;
|
|
||||||
|
|
||||||
// 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 matchesCooling && matchesHeating && matchesCost && checkAvailability(drone.id(), record);
|
return (
|
||||||
|
matchesCooling &&
|
||||||
|
matchesHeating &&
|
||||||
|
checkAvailability(drone.id(), record)
|
||||||
|
); // &&
|
||||||
|
// checkCost(drone, record) // checkCost is more expensive than checkAvailability
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -284,25 +208,213 @@ public class DroneInfoService {
|
||||||
* time
|
* time
|
||||||
* @return true if the drone is available, false otherwise
|
* @return true if the drone is available, false otherwise
|
||||||
*/
|
*/
|
||||||
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
private boolean checkAvailability(
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
String droneId,
|
||||||
|
MedDispatchRecRequest record
|
||||||
|
) {
|
||||||
|
URI droneUrl = URI.create(baseUrl).resolve(
|
||||||
|
dronesForServicePointsEndpoint
|
||||||
|
);
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
||||||
droneUrl,
|
droneUrl,
|
||||||
ServicePointDrones[].class);
|
ServicePointDrones[].class
|
||||||
|
);
|
||||||
|
|
||||||
LocalDate requiredDate = record.date();
|
LocalDate requiredDate = record.date();
|
||||||
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
||||||
LocalTime requiredTime = record.time();
|
LocalTime requiredTime = record.time();
|
||||||
|
|
||||||
|
assert servicePoints != null;
|
||||||
for (var servicePoint : servicePoints) {
|
for (var servicePoint : servicePoints) {
|
||||||
var drone = servicePoint.locateDroneById(droneId); // Nullable
|
var drone = servicePoint.locateDroneById(droneId); // Nullable
|
||||||
if (drone != null) {
|
if (drone != null) {
|
||||||
return drone.checkAvailability(requiredDay, requiredTime);
|
return drone.checkAvailability(requiredDay, requiredTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
||||||
|
URI droneUrl = URI.create(baseUrl).resolve(
|
||||||
|
dronesForServicePointsEndpoint
|
||||||
|
);
|
||||||
|
ServicePointDrones[] servicePoints = restTemplate.getForObject(
|
||||||
|
droneUrl,
|
||||||
|
ServicePointDrones[].class
|
||||||
|
);
|
||||||
|
|
||||||
|
assert servicePoints != null;
|
||||||
|
for (var sp : servicePoints) {
|
||||||
|
var drone = sp.locateDroneById(droneId); // Nullable
|
||||||
|
if (drone != null) {
|
||||||
|
return queryServicePointLocation(sp.servicePointId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private LngLat queryServicePointLocation(int id) {
|
||||||
|
URI servicePointUrl = URI.create(baseUrl).resolve(
|
||||||
|
servicePointsEndpoint
|
||||||
|
);
|
||||||
|
|
||||||
|
ServicePoint[] servicePoints = restTemplate.getForObject(
|
||||||
|
servicePointUrl,
|
||||||
|
ServicePoint[].class
|
||||||
|
);
|
||||||
|
|
||||||
|
assert servicePoints != null;
|
||||||
|
for (var sp : servicePoints) {
|
||||||
|
if (sp.id() == id) {
|
||||||
|
// We dont consider altitude
|
||||||
|
return new LngLat(sp.location());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// private Set<LngLat> parseObstacles() {
|
||||||
|
// URI restrictedAreasUrl = URI.create(baseUrl).resolve(
|
||||||
|
// 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);
|
||||||
|
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||||
|
return Arrays.asList(drones);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RestrictedArea> fetchRestrictedAreas() {
|
||||||
|
URI restrictedUrl = URI.create(baseUrl).resolve(
|
||||||
|
restrictedAreasEndpoint
|
||||||
|
);
|
||||||
|
RestrictedArea[] restrictedAreas = restTemplate.getForObject(
|
||||||
|
restrictedUrl,
|
||||||
|
RestrictedArea[].class
|
||||||
|
);
|
||||||
|
assert restrictedAreas != null;
|
||||||
|
List<RestrictedArea> restrictedAreaList = Arrays.asList(
|
||||||
|
restrictedAreas
|
||||||
|
);
|
||||||
|
return restrictedAreaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ServicePoint> fetchServicePoints() {
|
||||||
|
URI servicePointUrl = URI.create(baseUrl).resolve(
|
||||||
|
servicePointsEndpoint
|
||||||
|
);
|
||||||
|
ServicePoint[] servicePoints = restTemplate.getForObject(
|
||||||
|
servicePointUrl,
|
||||||
|
ServicePoint[].class
|
||||||
|
);
|
||||||
|
assert servicePoints != null;
|
||||||
|
List<ServicePoint> servicePointList = Arrays.asList(servicePoints);
|
||||||
|
return servicePointList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||||
|
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
|
||||||
|
dronesForServicePointsEndpoint
|
||||||
|
);
|
||||||
|
ServicePointDrones[] servicePointDrones = restTemplate.getForObject(
|
||||||
|
servicePointDronesUrl,
|
||||||
|
ServicePointDrones[].class
|
||||||
|
);
|
||||||
|
assert servicePointDrones != null;
|
||||||
|
List<ServicePointDrones> servicePointDronesList = Arrays.asList(
|
||||||
|
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,12 +1,11 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,6 +48,11 @@ public class GpsCalculationService {
|
||||||
return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);
|
return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double calculateSteps(LngLat position1, LngLat position2) {
|
||||||
|
double distance = calculateDistance(position1, position2);
|
||||||
|
return distance / STEP;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if {@code position1} and
|
* Check if {@code position1} and
|
||||||
* {@code position2} are close to each other, the threshold is < 0.00015
|
* {@code position2} are close to each other, the threshold is < 0.00015
|
||||||
|
|
@ -99,8 +103,10 @@ public class GpsCalculationService {
|
||||||
* @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) throws IllegalArgumentException {
|
public boolean checkIsInRegion(LngLat position, Region region)
|
||||||
if (!region.isClosed()) { // call method from RegionDto to check if not closed
|
throws IllegalArgumentException {
|
||||||
|
if (!region.isClosed()) {
|
||||||
|
// call method from RegionDto to check if not closed
|
||||||
throw new IllegalArgumentException("Region is not closed.");
|
throw new IllegalArgumentException("Region is not closed.");
|
||||||
}
|
}
|
||||||
return rayCasting(position, region.vertices());
|
return rayCasting(position, region.vertices());
|
||||||
|
|
@ -146,7 +152,10 @@ public class GpsCalculationService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double xIntersection = a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());
|
double xIntersection =
|
||||||
|
a.lng() +
|
||||||
|
((point.lat() - a.lat()) * (b.lng() - a.lng())) /
|
||||||
|
(b.lat() - a.lat());
|
||||||
|
|
||||||
if (xIntersection > point.lng()) {
|
if (xIntersection > point.lng()) {
|
||||||
++intersections;
|
++intersections;
|
||||||
|
|
@ -171,14 +180,19 @@ 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 = (p.lng() - a.lng()) * (b.lat() - a.lat())
|
double crossProduct =
|
||||||
- (p.lat() - a.lat()) * (b.lng() - a.lng());
|
(p.lng() - a.lng()) * (b.lat() - a.lat()) -
|
||||||
|
(p.lat() - a.lat()) * (b.lng() - a.lng());
|
||||||
if (Math.abs(crossProduct) > 1e-9) {
|
if (Math.abs(crossProduct) > 1e-9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isWithinLng = p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());
|
boolean isWithinLng =
|
||||||
boolean isWithinLat = p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());
|
p.lng() >= Math.min(a.lng(), b.lng()) &&
|
||||||
|
p.lng() <= Math.max(a.lng(), b.lng());
|
||||||
|
boolean isWithinLat =
|
||||||
|
p.lat() >= Math.min(a.lat(), b.lat()) &&
|
||||||
|
p.lat() <= Math.max(a.lat(), b.lat());
|
||||||
|
|
||||||
return isWithinLng && isWithinLat;
|
return isWithinLng && isWithinLat;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
|
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.RestrictedArea;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PathFinderService {
|
||||||
|
|
||||||
|
private final GpsCalculationService service;
|
||||||
|
|
||||||
|
public PathFinderService(GpsCalculationService gpsCalculationService) {
|
||||||
|
this.service = gpsCalculationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class Node implements Comparable<Node> {
|
||||||
|
|
||||||
|
final LngLat point;
|
||||||
|
Node parent;
|
||||||
|
|
||||||
|
double g;
|
||||||
|
double h;
|
||||||
|
double f;
|
||||||
|
|
||||||
|
public Node(LngLat point, Node parent, double g, double h) {
|
||||||
|
this.point = point;
|
||||||
|
this.parent = parent;
|
||||||
|
this.g = g;
|
||||||
|
this.h = h;
|
||||||
|
this.f = g + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Node other) {
|
||||||
|
return Double.compare(this.f, other.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static List<LngLat> findPath(
|
||||||
|
LngLat start,
|
||||||
|
LngLat target,
|
||||||
|
List<RestrictedArea> restricted
|
||||||
|
) {
|
||||||
|
var service = new GpsCalculationService();
|
||||||
|
PriorityQueue<Node> openSet = new PriorityQueue<>();
|
||||||
|
Map<LngLat, Double> allNodesMinG = new HashMap<>();
|
||||||
|
|
||||||
|
if (checkIsInRestrictedAreas(target, restricted)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Node startNode = new Node(
|
||||||
|
start,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
service.calculateDistance(start, target)
|
||||||
|
);
|
||||||
|
openSet.add(startNode);
|
||||||
|
allNodesMinG.put(start, 0.0);
|
||||||
|
|
||||||
|
while (!openSet.isEmpty()) {
|
||||||
|
Node current = openSet.poll();
|
||||||
|
|
||||||
|
if (service.isCloseTo(current.point, target)) {
|
||||||
|
return reconstructPath(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
current.g >
|
||||||
|
allNodesMinG.getOrDefault(current.point, Double.MAX_VALUE)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LngLat neighbour : getNeighbours(current.point)) {
|
||||||
|
if (checkIsInRestrictedAreas(neighbour, restricted)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double newG = current.g + 0.00015;
|
||||||
|
|
||||||
|
if (newG < allNodesMinG.getOrDefault(neighbour, Double.MAX_VALUE)) {
|
||||||
|
double newH = service.calculateDistance(neighbour, target);
|
||||||
|
Node neighbourNode = new Node(neighbour, current, newG, newH);
|
||||||
|
allNodesMinG.put(neighbour, newG);
|
||||||
|
openSet.add(neighbourNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LngLat> reconstructPath(Node endNode) {
|
||||||
|
LinkedList<LngLat> path = new LinkedList<>();
|
||||||
|
Node curr = endNode;
|
||||||
|
while (curr != null) {
|
||||||
|
path.addFirst(curr.point);
|
||||||
|
curr = curr.parent;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean checkIsInRestrictedAreas(
|
||||||
|
LngLat point,
|
||||||
|
List<RestrictedArea> RestrictedAreas
|
||||||
|
) {
|
||||||
|
var service = new GpsCalculationService();
|
||||||
|
for (RestrictedArea area : RestrictedAreas) {
|
||||||
|
Region r = area.toRegion();
|
||||||
|
if (service.checkIsInRegion(point, r)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LngLat> getNeighbours(LngLat p) {
|
||||||
|
var service = new GpsCalculationService();
|
||||||
|
double angle = 0;
|
||||||
|
List<LngLat> positions = new ArrayList<>();
|
||||||
|
final int directionCount = 8;
|
||||||
|
for (int i = 0; i < directionCount; i++) {
|
||||||
|
double directionAngle = angle + (i * 45);
|
||||||
|
LngLat nextPosition = service.nextPosition(p, directionAngle);
|
||||||
|
positions.add(nextPosition);
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,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 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;
|
||||||
|
|
@ -68,10 +66,7 @@ public class ApiControllerTest {
|
||||||
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(
|
service.calculateDistance(any(LngLat.class), any(LngLat.class))
|
||||||
any(LngLat.class),
|
|
||||||
any(LngLat.class)
|
|
||||||
)
|
|
||||||
).thenReturn(expected);
|
).thenReturn(expected);
|
||||||
var mock = mockMvc.perform(
|
var mock = mockMvc.perform(
|
||||||
post(endpoint)
|
post(endpoint)
|
||||||
|
|
@ -228,10 +223,7 @@ public class ApiControllerTest {
|
||||||
);
|
);
|
||||||
var req = new RegionCheckRequest(position, region);
|
var req = new RegionCheckRequest(position, region);
|
||||||
when(
|
when(
|
||||||
service.checkIsInRegion(
|
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
||||||
any(LngLat.class),
|
|
||||||
any(Region.class)
|
|
||||||
)
|
|
||||||
).thenReturn(expected);
|
).thenReturn(expected);
|
||||||
var mock = mockMvc.perform(
|
var mock = mockMvc.perform(
|
||||||
post(endpoint)
|
post(endpoint)
|
||||||
|
|
@ -253,10 +245,7 @@ public class ApiControllerTest {
|
||||||
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(
|
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
||||||
any(LngLat.class),
|
|
||||||
any(Region.class)
|
|
||||||
)
|
|
||||||
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
||||||
mockMvc
|
mockMvc
|
||||||
.perform(
|
.perform(
|
||||||
|
|
@ -286,10 +275,7 @@ public class ApiControllerTest {
|
||||||
);
|
);
|
||||||
var request = new RegionCheckRequest(position, region);
|
var request = new RegionCheckRequest(position, region);
|
||||||
when(
|
when(
|
||||||
service.checkIsInRegion(
|
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
||||||
any(LngLat.class),
|
|
||||||
any(Region.class)
|
|
||||||
)
|
|
||||||
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
||||||
mockMvc
|
mockMvc
|
||||||
.perform(
|
.perform(
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.LngLat;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.common.Region;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
public class GpsCalculationServiceTest {
|
public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
private static final double STEP = 0.00015;
|
private static final double STEP = 0.00015;
|
||||||
|
|
@ -29,6 +28,7 @@ public class GpsCalculationServiceTest {
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
|
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
|
||||||
class CalculateDistanceTests {
|
class CalculateDistanceTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("False: Given Example For Testing")
|
@DisplayName("False: Given Example For Testing")
|
||||||
void isCloseTo_shouldReturnFalse_givenExample() {
|
void isCloseTo_shouldReturnFalse_givenExample() {
|
||||||
|
|
@ -92,6 +92,7 @@ public class GpsCalculationServiceTest {
|
||||||
@Nested
|
@Nested
|
||||||
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
|
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
|
||||||
class IsCloseToTests {
|
class IsCloseToTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("False: Given Example For Testing")
|
@DisplayName("False: Given Example For Testing")
|
||||||
void isCloseTo_shouldReturnFalse_givenExample() {
|
void isCloseTo_shouldReturnFalse_givenExample() {
|
||||||
|
|
@ -112,7 +113,9 @@ public class GpsCalculationServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("True: Two points are close to each other and near threshold")
|
@DisplayName(
|
||||||
|
"True: Two points are close to each other and near threshold"
|
||||||
|
)
|
||||||
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
|
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);
|
||||||
|
|
@ -156,12 +159,20 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
|
@DisplayName(
|
||||||
|
"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);
|
||||||
double angle = 90;
|
double angle = 90;
|
||||||
|
|
@ -170,12 +181,20 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
|
@DisplayName(
|
||||||
|
"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);
|
||||||
double angle = 180;
|
double angle = 180;
|
||||||
|
|
@ -185,12 +204,20 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
|
@DisplayName(
|
||||||
|
"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);
|
||||||
double angle = 270;
|
double angle = 270;
|
||||||
|
|
@ -200,12 +227,20 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
|
@DisplayName(
|
||||||
|
"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);
|
||||||
double angle = 45;
|
double angle = 45;
|
||||||
|
|
@ -216,8 +251,14 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -227,14 +268,22 @@ public class GpsCalculationServiceTest {
|
||||||
// 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
|
// 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
|
||||||
double angle = 405;
|
double angle = 405;
|
||||||
double equivalentAngle = 45;
|
double equivalentAngle = 45;
|
||||||
double expectedLng = STEP * Math.cos(Math.toRadians(equivalentAngle));
|
double expectedLng =
|
||||||
double expectedLat = STEP * Math.sin(Math.toRadians(equivalentAngle));
|
STEP * Math.cos(Math.toRadians(equivalentAngle));
|
||||||
|
double expectedLat =
|
||||||
|
STEP * Math.sin(Math.toRadians(equivalentAngle));
|
||||||
var expected = new LngLat(expectedLng, expectedLat);
|
var expected = new LngLat(expectedLng, expectedLat);
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -249,8 +298,14 @@ public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
var actual = service.nextPosition(start, angle);
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
assertThat(actual.lng()).isCloseTo(
|
||||||
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
expected.lng(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
|
assertThat(actual.lat()).isCloseTo(
|
||||||
|
expected.lat(),
|
||||||
|
within(PRECISION)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,17 +313,31 @@ public class GpsCalculationServiceTest {
|
||||||
@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("rectangle", List.of(new LngLat(0.0, 0.0),
|
public static final Region RECTANGLE_REGION = new Region(
|
||||||
new LngLat(2.0, 0.0), new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, 0.0)));
|
"rectangle",
|
||||||
|
List.of(
|
||||||
|
new LngLat(0.0, 0.0),
|
||||||
|
new LngLat(2.0, 0.0),
|
||||||
|
new LngLat(2.0, 2.0),
|
||||||
|
new LngLat(0.0, 2.0),
|
||||||
|
new LngLat(0.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
@Test
|
@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("central",
|
var region = new Region(
|
||||||
List.of(new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.942617),
|
"central",
|
||||||
new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.946233),
|
List.of(
|
||||||
new LngLat(-3.192473, 55.946233)));
|
new LngLat(-3.192473, 55.946233),
|
||||||
|
new LngLat(-3.192473, 55.942617),
|
||||||
|
new LngLat(-3.184319, 55.942617),
|
||||||
|
new LngLat(-3.184319, 55.946233),
|
||||||
|
new LngLat(-3.192473, 55.946233)
|
||||||
|
)
|
||||||
|
);
|
||||||
boolean expected = false;
|
boolean expected = false;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -279,7 +348,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,7 +360,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,10 +371,18 @@ 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("hexagon",
|
var region = new Region(
|
||||||
List.of(new LngLat(1.0, 0.0), new LngLat(4.0, 0.0), new LngLat(5.0, 2.0),
|
"hexagon",
|
||||||
new LngLat(4.0, 4.0), new LngLat(1.0, 4.0), new LngLat(0.0, 2.0),
|
List.of(
|
||||||
new LngLat(1.0, 0.0)));
|
new LngLat(1.0, 0.0),
|
||||||
|
new LngLat(4.0, 0.0),
|
||||||
|
new LngLat(5.0, 2.0),
|
||||||
|
new LngLat(4.0, 4.0),
|
||||||
|
new LngLat(1.0, 4.0),
|
||||||
|
new LngLat(0.0, 2.0),
|
||||||
|
new LngLat(1.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -309,8 +392,15 @@ 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("triangle", List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0),
|
var region = new Region(
|
||||||
new LngLat(0.00005, 0.0001), new LngLat(0.0, 0.0)));
|
"triangle",
|
||||||
|
List.of(
|
||||||
|
new LngLat(0.0, 0.0),
|
||||||
|
new LngLat(0.0001, 0.0),
|
||||||
|
new LngLat(0.00005, 0.0001),
|
||||||
|
new LngLat(0.0, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
boolean expected = true;
|
boolean expected = true;
|
||||||
boolean actual = service.checkIsInRegion(position, region);
|
boolean actual = service.checkIsInRegion(position, region);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
|
@ -321,7 +411,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,7 +423,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,7 +435,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +447,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,7 +459,10 @@ 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(position, RECTANGLE_REGION);
|
boolean actual = service.checkIsInRegion(
|
||||||
|
position,
|
||||||
|
RECTANGLE_REGION
|
||||||
|
);
|
||||||
assertThat(actual).isEqualTo(expected);
|
assertThat(actual).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,22 +470,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("line",
|
var region = new Region(
|
||||||
List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0), new LngLat(0.0, 0.0)));
|
"line",
|
||||||
|
List.of(
|
||||||
|
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).hasMessage("Region is not closed.");
|
})
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.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("rectangle", List.of(new LngLat(0.0, 0.0), new LngLat(2.0, 0.0),
|
var region = new Region(
|
||||||
new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, -1.0)));
|
"rectangle",
|
||||||
|
List.of(
|
||||||
|
new LngLat(0.0, 0.0),
|
||||||
|
new LngLat(2.0, 0.0),
|
||||||
|
new LngLat(2.0, 2.0),
|
||||||
|
new LngLat(0.0, 2.0),
|
||||||
|
new LngLat(0.0, -1.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
assertThatThrownBy(() -> {
|
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -390,7 +513,9 @@ public class GpsCalculationServiceTest {
|
||||||
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue