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 {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ body:json {
|
|||
]
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ body:json {
|
|||
]
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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.Region;
|
||||
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||
|
|
@ -79,7 +80,7 @@ public class ApiController {
|
|||
@PostMapping("/nextPosition")
|
||||
public LngLat getNextPosition(@RequestBody MovementRequest request) {
|
||||
LngLat start = request.start();
|
||||
double angle = request.angle();
|
||||
Angle angle = new Angle(request.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
|
||||
*/
|
||||
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) {
|
||||
this(coord.lng(), coord.lat());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
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 java.util.Arrays;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents the data transfer object for a region definition
|
||||
* <p>
|
||||
|
|
@ -48,4 +51,33 @@ public record Region(String name, List<LngLat> vertices) {
|
|||
LngLat last = vertices.getLast();
|
||||
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;
|
||||
|
||||
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.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.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
|
|
@ -28,7 +23,6 @@ import org.springframework.web.client.RestTemplate;
|
|||
public class DroneInfoService {
|
||||
|
||||
private final String baseUrl;
|
||||
private final String dronesEndpoint = "drones";
|
||||
private final String dronesForServicePointsEndpoint =
|
||||
"drones-for-service-points";
|
||||
public static final String servicePointsEndpoint = "service-points";
|
||||
|
|
@ -60,7 +54,7 @@ public class DroneInfoService {
|
|||
*
|
||||
* @param state determines the capability filtering
|
||||
* @return if {@code state} is true, return ids of drones with cooling
|
||||
* capability, else without cooling
|
||||
* capability, else without cooling
|
||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||
*/
|
||||
public List<String> dronesWithCooling(boolean state) {
|
||||
|
|
@ -96,10 +90,6 @@ public class DroneInfoService {
|
|||
public Drone droneDetail(String id) {
|
||||
List<Drone> drones = fetchAllDrones();
|
||||
|
||||
if (drones == null) {
|
||||
throw new NullPointerException("drone cannot be found");
|
||||
}
|
||||
|
||||
for (var drone : drones) {
|
||||
if (drone.id().equals(id)) {
|
||||
return drone;
|
||||
|
|
@ -119,16 +109,12 @@ public class DroneInfoService {
|
|||
* Associated service method with
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
|
||||
List<Drone> drones = fetchAllDrones();
|
||||
|
||||
if (drones == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (rec == null || rec.length == 0) {
|
||||
return drones
|
||||
.stream()
|
||||
|
|
@ -146,7 +132,7 @@ public class DroneInfoService {
|
|||
.filter(d ->
|
||||
Arrays.stream(rec)
|
||||
.filter(r -> r != null && r.requirements() != null)
|
||||
.allMatch(r -> meetsRequirement(d, r))
|
||||
.allMatch(r -> droneMatchesRequirement(d, r))
|
||||
)
|
||||
.map(Drone::id)
|
||||
.collect(Collectors.toList());
|
||||
|
|
@ -162,7 +148,10 @@ public class DroneInfoService {
|
|||
* is invalid (capacity and id cannot be null
|
||||
* in {@code MedDispathRecDto})
|
||||
*/
|
||||
public boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
|
||||
public boolean droneMatchesRequirement(
|
||||
Drone drone,
|
||||
MedDispatchRecRequest record
|
||||
) {
|
||||
var requirements = record.requirements();
|
||||
if (requirements == null) {
|
||||
throw new IllegalArgumentException("requirements cannot be null");
|
||||
|
|
@ -184,7 +173,8 @@ public class DroneInfoService {
|
|||
boolean requiredHeating = requirements.heating();
|
||||
|
||||
// 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
|
||||
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
|
||||
boolean matchesCooling = !requiredCooling || capability.cooling();
|
||||
|
|
@ -197,7 +187,8 @@ public class DroneInfoService {
|
|||
matchesHeating &&
|
||||
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;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
public List<Drone> fetchAllDrones() {
|
||||
String dronesEndpoint = "drones";
|
||||
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
|
||||
assert drones != null;
|
||||
return Arrays.asList(drones);
|
||||
}
|
||||
|
||||
private List<RestrictedArea> fetchRestrictedAreas() {
|
||||
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||
URI restrictedUrl = URI.create(baseUrl).resolve(
|
||||
restrictedAreasEndpoint
|
||||
);
|
||||
|
|
@ -350,13 +284,22 @@ public class DroneInfoService {
|
|||
RestrictedArea[].class
|
||||
);
|
||||
assert restrictedAreas != null;
|
||||
List<RestrictedArea> restrictedAreaList = Arrays.asList(
|
||||
restrictedAreas
|
||||
);
|
||||
return restrictedAreaList;
|
||||
return Arrays.asList(restrictedAreas);
|
||||
}
|
||||
|
||||
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(
|
||||
servicePointsEndpoint
|
||||
);
|
||||
|
|
@ -365,11 +308,10 @@ public class DroneInfoService {
|
|||
ServicePoint[].class
|
||||
);
|
||||
assert servicePoints != null;
|
||||
List<ServicePoint> servicePointList = Arrays.asList(servicePoints);
|
||||
return servicePointList;
|
||||
return Arrays.asList(servicePoints);
|
||||
}
|
||||
|
||||
private List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
|
||||
dronesForServicePointsEndpoint
|
||||
);
|
||||
|
|
@ -378,43 +320,6 @@ public class DroneInfoService {
|
|||
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;
|
||||
return Arrays.asList(servicePointDrones);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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.Region;
|
||||
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||
|
|
@ -84,8 +85,8 @@ public class GpsCalculationService {
|
|||
* @see #STEP
|
||||
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
|
||||
*/
|
||||
public LngLat nextPosition(LngLat start, double angle) {
|
||||
double rad = Math.toRadians(angle); // Convert to radian for Java triangle function calculation
|
||||
public LngLat nextPosition(LngLat start, Angle angle) {
|
||||
double rad = angle.toRadians();
|
||||
double newLng = Math.cos(rad) * STEP + start.lng();
|
||||
double newLat = Math.sin(rad) * STEP + start.lat();
|
||||
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 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.Region;
|
||||
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
|
||||
|
|
@ -101,6 +102,25 @@ public class ApiControllerTest {
|
|||
)
|
||||
.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
|
||||
|
|
@ -146,6 +166,25 @@ public class ApiControllerTest {
|
|||
)
|
||||
.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
|
||||
|
|
@ -162,7 +201,7 @@ public class ApiControllerTest {
|
|||
LngLat p = new LngLat(0, 0);
|
||||
var req = new MovementRequest(p, 0);
|
||||
when(
|
||||
service.nextPosition(any(LngLat.class), anyDouble())
|
||||
service.nextPosition(any(LngLat.class), any(Angle.class))
|
||||
).thenReturn(expected);
|
||||
var mock = mockMvc.perform(
|
||||
post(endpoint)
|
||||
|
|
@ -187,7 +226,7 @@ public class ApiControllerTest {
|
|||
"angle": 180
|
||||
}
|
||||
""";
|
||||
when(service.nextPosition(isNull(), anyDouble())).thenThrow(
|
||||
when(service.nextPosition(isNull(), any(Angle.class))).thenThrow(
|
||||
new NullPointerException()
|
||||
);
|
||||
mockMvc
|
||||
|
|
@ -198,6 +237,32 @@ public class ApiControllerTest {
|
|||
)
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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.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.Region;
|
||||
import java.util.List;
|
||||
|
|
@ -52,7 +53,7 @@ public class GpsCalculationServiceTest {
|
|||
@Test
|
||||
@DisplayName("Edge Case: Points are Identical")
|
||||
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 actual = service.calculateDistance(p1, p1);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
|
|
@ -61,8 +62,8 @@ public class GpsCalculationServiceTest {
|
|||
@Test
|
||||
@DisplayName("Edge Case: Longitudinal-only movement")
|
||||
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() {
|
||||
var p1 = new LngLat(123.85, 983.2119);
|
||||
var p2 = new LngLat(133.85, 983.2119);
|
||||
var p1 = new LngLat(23.85, 83.2119);
|
||||
var p2 = new LngLat(33.85, 83.2119);
|
||||
double expected = 10.0;
|
||||
double actual = service.calculateDistance(p1, p2);
|
||||
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||
|
|
@ -71,8 +72,8 @@ public class GpsCalculationServiceTest {
|
|||
@Test
|
||||
@DisplayName("Edge Case: Latitude-only movement")
|
||||
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() {
|
||||
var p1 = new LngLat(123.85, 983.2119);
|
||||
var p2 = new LngLat(123.85, 973.2119);
|
||||
var p1 = new LngLat(123.85, 68.2119);
|
||||
var p2 = new LngLat(123.85, 58.2119);
|
||||
double expected = 10.0;
|
||||
double actual = service.calculateDistance(p1, p2);
|
||||
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||
|
|
@ -106,7 +107,7 @@ public class GpsCalculationServiceTest {
|
|||
@Test
|
||||
@DisplayName("True: Two points are the same")
|
||||
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
|
||||
var p1 = new LngLat(151.86, 285.37);
|
||||
var p1 = new LngLat(15.86, 28.37);
|
||||
boolean expected = true;
|
||||
boolean actual = service.isCloseTo(p1, p1);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
|
|
@ -153,7 +154,7 @@ public class GpsCalculationServiceTest {
|
|||
@DisplayName("General Case: nextPosition in East direction (0 degrees)")
|
||||
void nextPosition_shouldMoveEast_forAngleZero() {
|
||||
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.
|
||||
var expected = new LngLat(STEP, 0.0);
|
||||
|
||||
|
|
@ -175,7 +176,7 @@ public class GpsCalculationServiceTest {
|
|||
)
|
||||
void nextPosition_shouldMoveNorth_forAngle90() {
|
||||
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.
|
||||
var expected = new LngLat(0.0, STEP);
|
||||
|
||||
|
|
@ -197,7 +198,7 @@ public class GpsCalculationServiceTest {
|
|||
)
|
||||
void nextPosition_shouldMoveWest_forAngle180() {
|
||||
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
|
||||
// lng axis.
|
||||
var expected = new LngLat(-STEP, 0.0);
|
||||
|
|
@ -220,7 +221,7 @@ public class GpsCalculationServiceTest {
|
|||
)
|
||||
void nextPosition_shouldMoveSouth_forAngle270() {
|
||||
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
|
||||
// lat axis.
|
||||
var expected = new LngLat(0.0, -STEP);
|
||||
|
|
@ -243,10 +244,10 @@ public class GpsCalculationServiceTest {
|
|||
)
|
||||
void nextPosition_shouldMoveNortheast_forAngle45() {
|
||||
var start = new LngLat(0.0, 0.0);
|
||||
double angle = 45;
|
||||
Angle angle = new Angle(45);
|
||||
// Δlng = step * cos(45°), Δlat = step * sin(45°)
|
||||
double expectedLng = STEP * Math.cos(Math.toRadians(angle));
|
||||
double expectedLat = STEP * Math.sin(Math.toRadians(angle));
|
||||
double expectedLng = STEP * Math.cos(angle.toRadians());
|
||||
double expectedLat = STEP * Math.sin(angle.toRadians());
|
||||
var expected = new LngLat(expectedLng, expectedLat);
|
||||
|
||||
var actual = service.nextPosition(start, angle);
|
||||
|
|
@ -261,261 +262,216 @@ 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);
|
||||
@Nested
|
||||
@DisplayName(
|
||||
"Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean"
|
||||
)
|
||||
class CheckIsInRegionTests {
|
||||
|
||||
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
|
||||
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
|
||||
class CheckIsInRegionTests {
|
||||
|
||||
public static final Region RECTANGLE_REGION = new Region(
|
||||
"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
|
||||
@DisplayName("General Case: Given Example for Testing")
|
||||
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
|
||||
var position = new LngLat(1.234, 1.222);
|
||||
var region = new Region(
|
||||
"central",
|
||||
List.of(
|
||||
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 actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Rectangle")
|
||||
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
|
||||
var position = new LngLat(1.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Rectangle")
|
||||
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
|
||||
var position = new LngLat(3.0, 1.0);
|
||||
boolean expected = false;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Hexagon")
|
||||
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region(
|
||||
"hexagon",
|
||||
List.of(
|
||||
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 actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Small Triangle")
|
||||
void isInRegion_shouldReturnTrue_forSmallTriangle() {
|
||||
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),
|
||||
new LngLat(0.00005, 0.0001),
|
||||
new LngLat(0.0, 0.0)
|
||||
)
|
||||
);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Lower Edge of Rectangle")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
|
||||
var position = new LngLat(0.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Upper Edge of Rectangle")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
|
||||
var position = new LngLat(2.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Left Edge")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
|
||||
var position = new LngLat(0.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Lower Vertex")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
|
||||
var position = new LngLat(0.0, 0.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Upper Vertex")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Region not forming polygon")
|
||||
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region(
|
||||
"line",
|
||||
List.of(
|
||||
new LngLat(0.0, 0.0),
|
||||
new LngLat(0.0001, 0.0),
|
||||
new LngLat(0.0, 0.0)
|
||||
)
|
||||
);
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Region is not closed")
|
||||
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region(
|
||||
public static final Region RECTANGLE_REGION = new Region(
|
||||
"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)
|
||||
new LngLat(0.0, 0.0)
|
||||
)
|
||||
);
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Vertex list is empty")
|
||||
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region("rectangle", List.of());
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
@Test
|
||||
@DisplayName("General Case: Given Example for Testing")
|
||||
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
|
||||
var position = new LngLat(1.234, 1.222);
|
||||
var region = new Region(
|
||||
"central",
|
||||
List.of(
|
||||
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 actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Rectangle")
|
||||
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
|
||||
var position = new LngLat(1.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Rectangle")
|
||||
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
|
||||
var position = new LngLat(3.0, 1.0);
|
||||
boolean expected = false;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("General Case: Simple Hexagon")
|
||||
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region(
|
||||
"hexagon",
|
||||
List.of(
|
||||
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 actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Small Triangle")
|
||||
void isInRegion_shouldReturnTrue_forSmallTriangle() {
|
||||
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),
|
||||
new LngLat(0.00005, 0.0001),
|
||||
new LngLat(0.0, 0.0)
|
||||
)
|
||||
);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(position, region);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Lower Edge of Rectangle")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
|
||||
var position = new LngLat(0.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Upper Edge of Rectangle")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
|
||||
var position = new LngLat(2.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Left Edge")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
|
||||
var position = new LngLat(0.0, 1.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Lower Vertex")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
|
||||
var position = new LngLat(0.0, 0.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Point on Upper Vertex")
|
||||
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
boolean expected = true;
|
||||
boolean actual = service.checkIsInRegion(
|
||||
position,
|
||||
RECTANGLE_REGION
|
||||
);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Region not forming polygon")
|
||||
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region(
|
||||
"line",
|
||||
List.of(
|
||||
new LngLat(0.0, 0.0),
|
||||
new LngLat(0.0001, 0.0),
|
||||
new LngLat(0.0, 0.0)
|
||||
)
|
||||
);
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Region is not closed")
|
||||
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
|
||||
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),
|
||||
new LngLat(2.0, 2.0),
|
||||
new LngLat(0.0, 2.0),
|
||||
new LngLat(0.0, -1.0)
|
||||
)
|
||||
);
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Edge Case: Vertex list is empty")
|
||||
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
|
||||
var position = new LngLat(2.0, 2.0);
|
||||
var region = new Region("rectangle", List.of());
|
||||
assertThatThrownBy(() -> {
|
||||
service.checkIsInRegion(position, region);
|
||||
})
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Region is not closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue