diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
index 93b23fa..d09f546 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Complex.bru
@@ -43,6 +43,10 @@ body:json {
]
}
+assert {
+ res.status: eq 200
+}
+
settings {
encodeUrl: true
timeout: 0
diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Example.bru b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
index 9fed8e5..1e65620 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Example.bru
@@ -30,6 +30,10 @@ body:json {
]
}
+assert {
+ res.status: eq 200
+}
+
settings {
encodeUrl: true
timeout: 0
diff --git a/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
index c629adc..94a4326 100644
--- a/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
+++ b/ilp-cw-api/[POST] queryAvailableDrones/Treat Null as False (Cooling).bru
@@ -26,6 +26,10 @@ body:json {
]
}
+assert {
+ res.status: eq 200
+}
+
settings {
encodeUrl: true
timeout: 0
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java b/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
index 05be9ea..1fba40b 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/controller/ApiController.java
@@ -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);
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java b/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java
new file mode 100644
index 0000000..f637e55
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/controller/GeoJsonDataController.java
@@ -0,0 +1,4 @@
+package io.github.js0ny.ilp_coursework.controller;
+
+public class GeoJsonDataController {
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java
new file mode 100644
index 0000000..ea07457
--- /dev/null
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Angle.java
@@ -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);
+ }
+}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
index 9a4ee50..6208b22 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/LngLat.java
@@ -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());
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
index 740a1d1..ff002a4 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/data/common/Region.java
@@ -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
*
@@ -48,4 +51,33 @@ public record Region(String name, List vertices) {
LngLat last = vertices.getLast();
return Objects.equals(last, first);
}
+
+ public Map toGeoJson() {
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+
+ List> 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);
+ }
+ }
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
index 6ecd613..829fff6 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java
@@ -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 dronesWithCooling(boolean state) {
@@ -96,10 +90,6 @@ public class DroneInfoService {
public Drone droneDetail(String id) {
List 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 dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
List 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 parseObstacles() {
- // URI restrictedAreasUrl = URI.create(baseUrl).resolve(
- // restrictedAreasEndpoint
- // );
- //
- // RestrictedArea[] restrictedAreas = restTemplate.getForObject(
- // restrictedAreasUrl,
- // RestrictedArea[].class
- // );
- //
- // assert restrictedAreas != null;
- // Set 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 restrictedAreas = fetchRestrictedAreas();
- // List totalPath = new ArrayList<>();
- // List 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 shortestPath = null;
- // for (var d : possibleDrones) {
- // var start = queryServicePointLocationByDroneId(d.id());
- // List 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 fetchAllDrones() {
+ public List 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 fetchRestrictedAreas() {
+ public List fetchRestrictedAreas() {
URI restrictedUrl = URI.create(baseUrl).resolve(
restrictedAreasEndpoint
);
@@ -350,13 +284,22 @@ public class DroneInfoService {
RestrictedArea[].class
);
assert restrictedAreas != null;
- List restrictedAreaList = Arrays.asList(
- restrictedAreas
- );
- return restrictedAreaList;
+ return Arrays.asList(restrictedAreas);
}
- private List fetchServicePoints() {
+ public List 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 fetchServicePoints() {
URI servicePointUrl = URI.create(baseUrl).resolve(
servicePointsEndpoint
);
@@ -365,11 +308,10 @@ public class DroneInfoService {
ServicePoint[].class
);
assert servicePoints != null;
- List servicePointList = Arrays.asList(servicePoints);
- return servicePointList;
+ return Arrays.asList(servicePoints);
}
- private List fetchDronesForServicePoints() {
+ public List fetchDronesForServicePoints() {
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
@@ -378,43 +320,6 @@ public class DroneInfoService {
ServicePointDrones[].class
);
assert servicePointDrones != null;
- List 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);
}
}
diff --git a/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java b/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
index 3cf22b7..2e3abd1 100644
--- a/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
+++ b/src/main/java/io/github/js0ny/ilp_coursework/service/GpsCalculationService.java
@@ -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);
diff --git a/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java b/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
index d99dc0d..abd310f 100644
--- a/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
+++ b/src/test/java/io/github/js0ny/ilp_coursework/controller/ApiControllerTest.java
@@ -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
diff --git a/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java b/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
index 8edd77f..fe9ce8c 100644
--- a/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
+++ b/src/test/java/io/github/js0ny/ilp_coursework/service/GpsCalculationServiceTest.java
@@ -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.");
+ }
}
}
}