test: Add GpsCalculationServiceTest.java
This commit is contained in:
parent
0e87787beb
commit
26e1c80326
11 changed files with 569 additions and 18 deletions
|
|
@ -6,10 +6,11 @@ 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.dto.DistanceRequestDto;
|
import io.github.js0ny.ilp_coursework.data.DistanceRequestDto;
|
||||||
import io.github.js0ny.ilp_coursework.dto.LngLatDto;
|
import io.github.js0ny.ilp_coursework.data.LngLatDto;
|
||||||
import io.github.js0ny.ilp_coursework.dto.MovementRequestDto;
|
import io.github.js0ny.ilp_coursework.data.MovementRequestDto;
|
||||||
import io.github.js0ny.ilp_coursework.dto.RegionCheckRequestDto;
|
import io.github.js0ny.ilp_coursework.data.RegionCheckRequestDto;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.RegionDto;
|
||||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -52,6 +53,8 @@ public class ApiController {
|
||||||
|
|
||||||
@PostMapping("/isInRegion")
|
@PostMapping("/isInRegion")
|
||||||
public boolean getIsInRegion(@RequestBody RegionCheckRequestDto request) {
|
public boolean getIsInRegion(@RequestBody RegionCheckRequestDto request) {
|
||||||
return true;
|
LngLatDto position = request.position();
|
||||||
|
RegionDto region = request.region();
|
||||||
|
return gpsService.checkIsInRegion(position, region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package io.github.js0ny.ilp_coursework.dto;
|
package io.github.js0ny.ilp_coursework.data;
|
||||||
|
|
||||||
public record DistanceRequestDto(LngLatDto position1, LngLatDto position2) {
|
public record DistanceRequestDto(LngLatDto position1, LngLatDto position2) {
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package io.github.js0ny.ilp_coursework.dto;
|
package io.github.js0ny.ilp_coursework.data;
|
||||||
|
|
||||||
public record LngLatDto(double lng, double lat) {
|
public record LngLatDto(double lng, double lat) {
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package io.github.js0ny.ilp_coursework.dto;
|
package io.github.js0ny.ilp_coursework.data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the data transfer object for a movement action request.
|
* Represents the data transfer object for a movement action request.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.data;
|
||||||
|
|
||||||
|
public record RegionCheckRequestDto(LngLatDto position, RegionDto region) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package io.github.js0ny.ilp_coursework.dto;
|
package io.github.js0ny.ilp_coursework.data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record RegionDto(String name, List<LngLatDto> vertices) {
|
public record RegionDto(String name, List<LngLatDto> vertices) {
|
||||||
|
|
||||||
public boolean isClose() {
|
public boolean isClosed() {
|
||||||
// Magic number 4: For a polygon, 3 edges is required.
|
// Magic number 4: For a polygon, 3 edges is required.
|
||||||
// In this dto, edges + 1 vertices is required.
|
// In this dto, edges + 1 vertices is required.
|
||||||
if (vertices == null || vertices.size() < 4) {
|
if (vertices == null || vertices.size() < 4) {
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package io.github.js0ny.ilp_coursework.dto;
|
|
||||||
|
|
||||||
public record RegionCheckRequestDto(LngLatDto position) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,34 @@
|
||||||
package io.github.js0ny.ilp_coursework.exception;
|
package io.github.js0ny.ilp_coursework.exception;
|
||||||
|
|
||||||
public class GlobalExceptionHandler {
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Map<String, String> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
|
||||||
|
return Map.of("status", "400", "error", "Invalid JSON request body.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public Map<String, String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
|
String errorMessage = Optional.ofNullable(ex.getMessage())
|
||||||
|
.orElse("Invalid argument provided.");
|
||||||
|
return Map.of("status", "400", "error", errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ExceptionHandler(NullPointerException.class)
|
||||||
|
// @ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
// public Map<String, String> handleNullPointerException(NullPointerException
|
||||||
|
// ex) {
|
||||||
|
// return Map.of("error", "Invalid JSON request body.");
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.LngLatDto;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.RegionDto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.dto.LngLatDto;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GpsCalculationService {
|
public class GpsCalculationService {
|
||||||
|
|
||||||
private static final double STEP = 0.00015;
|
private static final double STEP = 0.00015;
|
||||||
private static final double CLOSE_THRESHOLD = STEP;
|
private static final double CLOSE_THRESHOLD = STEP;
|
||||||
|
|
||||||
|
|
@ -20,6 +24,16 @@ public class GpsCalculationService {
|
||||||
return distance < CLOSE_THRESHOLD;
|
return distance < CLOSE_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from <code>ApiController.getNextPosition</code>.
|
||||||
|
* <p>
|
||||||
|
* Returns the next position moved from <code>start</code> in the direction with <code>angle</code>, with step size
|
||||||
|
* 0.00015
|
||||||
|
*
|
||||||
|
* @param start The coordinate of the original start point.
|
||||||
|
* @param angle The direction to be moved in angle.
|
||||||
|
* @return The next position moved from <code>start</code>
|
||||||
|
*/
|
||||||
public LngLatDto nextPosition(LngLatDto start, double angle) {
|
public LngLatDto nextPosition(LngLatDto start, double angle) {
|
||||||
double rad = Math.toRadians(angle);
|
double rad = Math.toRadians(angle);
|
||||||
double newLng = Math.cos(rad) * STEP + start.lng();
|
double newLng = Math.cos(rad) * STEP + start.lng();
|
||||||
|
|
@ -27,4 +41,100 @@ public class GpsCalculationService {
|
||||||
return new LngLatDto(newLng, newLat);
|
return new LngLatDto(newLng, newLat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from <code>ApiController.getIsInRegion</code>.
|
||||||
|
* <p>
|
||||||
|
* Used to check if the given <code>position</code>
|
||||||
|
* is inside the <code>region</code>, on edge and vertex is considered as inside.
|
||||||
|
*
|
||||||
|
* @param position The coordinate of the position.
|
||||||
|
* @param region A <code>RegionDto</code> that contains name and a list of <code>LngLatDto</code>
|
||||||
|
* @return true if <code>position</code> is inside the <code>region</code>.
|
||||||
|
* @throws IllegalArgumentException If <code>region</code> is not closed
|
||||||
|
*/
|
||||||
|
public boolean checkIsInRegion(LngLatDto position, RegionDto region) throws IllegalArgumentException {
|
||||||
|
if (!region.isClosed()) { // call method from RegionDto to check if not closed
|
||||||
|
throw new IllegalArgumentException("Region is not closed.");
|
||||||
|
}
|
||||||
|
return rayCasting(position, region.vertices());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to <code>checkIsInRegion</code>, use of ray-casting algorithm
|
||||||
|
* to check if inside the polygon
|
||||||
|
*
|
||||||
|
* @param point The point to check
|
||||||
|
* @param polygon The region that forms a polygon to check if <code>point</code>
|
||||||
|
* sits inside.
|
||||||
|
* @return If the <code>point</code> sits inside the <code>polygon</code> then
|
||||||
|
* return True
|
||||||
|
*/
|
||||||
|
private boolean rayCasting(LngLatDto point, List<LngLatDto> polygon) {
|
||||||
|
int intersections = 0;
|
||||||
|
int n = polygon.size();
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
LngLatDto a = polygon.get(i);
|
||||||
|
LngLatDto b = polygon.get((i + 1) % n); // Next vertex
|
||||||
|
|
||||||
|
if (isPointOnEdge(point, a, b)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a is norther than b, in order to easy classification
|
||||||
|
if (a.lat() > b.lat()) {
|
||||||
|
LngLatDto temp = a;
|
||||||
|
a = b;
|
||||||
|
b = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The point is not between a and b in latitude mean, skip this loop
|
||||||
|
if (point.lat() < a.lat() || point.lat() >= b.lat()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the case of horizontal edge, already handled in `isPointOnEdge`:w
|
||||||
|
if (a.lat() == b.lat()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double xIntersection = a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat());
|
||||||
|
|
||||||
|
// // The point is on the edge
|
||||||
|
// if (xIntersection == point.lng()) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (xIntersection > point.lng()) {
|
||||||
|
++intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If intersections are odd, ray-casting returns true, which the point sits
|
||||||
|
// inside the polygon;
|
||||||
|
// If intersections are even, the point does not sit inside the polygon.
|
||||||
|
return intersections % 2 == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function from <code>rayCasting</code> that used to simply calculation <br>
|
||||||
|
* Used to check if point <code>p</code> is on the edge formed by <code>a</code> and <code>b</code>
|
||||||
|
*
|
||||||
|
* @param p point to be checked on the edge
|
||||||
|
* @param a point that forms the edge
|
||||||
|
* @param b point that forms the edge
|
||||||
|
* @return boolean, if <code>p</code> is on <code>ab</code> then true
|
||||||
|
*/
|
||||||
|
private boolean isPointOnEdge(LngLatDto p, LngLatDto a, LngLatDto b) {
|
||||||
|
// Cross product: (p - a) × (b - a)
|
||||||
|
double crossProduct = (p.lng() - a.lng()) * (b.lat() - a.lat())
|
||||||
|
- (p.lat() - a.lat()) * (b.lng() - a.lng());
|
||||||
|
if (Math.abs(crossProduct) > 1e-9) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isWithinLng = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.DistanceRequestDto;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.LngLatDto;
|
||||||
|
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||||
|
|
||||||
|
@WebMvcTest(ApiController.class)
|
||||||
|
public class ApiControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
private GpsCalculationService gpsCalculationService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getUid_shouldReturnStudentIdFromService() throws Exception {
|
||||||
|
String endpoint = "/api/v1/uid";
|
||||||
|
String expected = "s2522255";
|
||||||
|
var mock = mockMvc.perform(get(endpoint));
|
||||||
|
mock.andExpect(MockMvcResultMatchers.status().isOk());
|
||||||
|
mock.andExpect(MockMvcResultMatchers.content().string(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDistance_shouldReturnDoubleFromService_whenCorrectInput() throws Exception {
|
||||||
|
double expected = 5.0;
|
||||||
|
String endpoint = "/api/v1/distanceTo";
|
||||||
|
LngLatDto p1 = new LngLatDto(0, 4.0);
|
||||||
|
LngLatDto p2 = new LngLatDto(3.0, 0);
|
||||||
|
var req = new DistanceRequestDto(p1, p2);
|
||||||
|
var mock = mockMvc.perform(
|
||||||
|
post(endpoint)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(req))
|
||||||
|
);
|
||||||
|
|
||||||
|
mock.andExpect(MockMvcResultMatchers.status().isOk());
|
||||||
|
mock.andExpect(MockMvcResultMatchers.content().string(String.valueOf(expected)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.LngLatDto;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.RegionDto;
|
||||||
|
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.AssertionsForClassTypes.within;
|
||||||
|
|
||||||
|
|
||||||
|
public class GpsCalculationServiceTest {
|
||||||
|
|
||||||
|
private static final double STEP = 0.00015;
|
||||||
|
private static final double CLOSE_THRESHOLD = STEP;
|
||||||
|
private static final double PRECISION = 1e-9;
|
||||||
|
|
||||||
|
private GpsCalculationService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpService() {
|
||||||
|
service = new GpsCalculationService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
|
||||||
|
class CalculateDistanceTests {
|
||||||
|
@Test
|
||||||
|
@DisplayName("False: Given Example For Testing")
|
||||||
|
void isCloseTo_shouldReturnFalse_givenExample() {
|
||||||
|
var p1 = new LngLatDto(-3.192473, 55.946233);
|
||||||
|
var p2 = new LngLatDto(-3.192473, 55.942617);
|
||||||
|
double expected = 0.0036;
|
||||||
|
double actual = service.calculateDistance(p1, p2);
|
||||||
|
assertThat(actual).isCloseTo(expected, within(1e-4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("General Case: 3-4-5 Triangle")
|
||||||
|
void calculateDistance_shouldReturnCorrectEuclideanDistance_forGeneralCase() {
|
||||||
|
var p1 = new LngLatDto(0, 3.0);
|
||||||
|
var p2 = new LngLatDto(4.0, 0);
|
||||||
|
double expected = 5.0;
|
||||||
|
double actual = service.calculateDistance(p1, p2);
|
||||||
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Edge Case: Points are Identical")
|
||||||
|
void calculateDistance_shouldReturnZero_whenPointsAreIdentical() {
|
||||||
|
var p1 = new LngLatDto(123.85, 983.2119);
|
||||||
|
double expected = 0.0;
|
||||||
|
double actual = service.calculateDistance(p1, p1);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Edge Case: Longitudinal-only movement")
|
||||||
|
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() {
|
||||||
|
var p1 = new LngLatDto(123.85, 983.2119);
|
||||||
|
var p2 = new LngLatDto(133.85, 983.2119);
|
||||||
|
double expected = 10.0;
|
||||||
|
double actual = service.calculateDistance(p1, p2);
|
||||||
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Edge Case: Latitude-only movement")
|
||||||
|
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() {
|
||||||
|
var p1 = new LngLatDto(123.85, 983.2119);
|
||||||
|
var p2 = new LngLatDto(123.85, 973.2119);
|
||||||
|
double expected = 10.0;
|
||||||
|
double actual = service.calculateDistance(p1, p2);
|
||||||
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("General Case: Calculate with negative Coordinates")
|
||||||
|
void calculateDistance_shouldReturnCorrectEuclideanDistance_forNegativeCoordinates() {
|
||||||
|
LngLatDto p1 = new LngLatDto(-1.0, -2.0);
|
||||||
|
LngLatDto p2 = new LngLatDto(2.0, 2.0); // lngDiff = 3, latDiff = 4
|
||||||
|
double expected = 5.0;
|
||||||
|
double actual = service.calculateDistance(p1, p2);
|
||||||
|
assertThat(actual).isCloseTo(expected, within(PRECISION));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
|
||||||
|
class IsCloseToTests {
|
||||||
|
@Test
|
||||||
|
@DisplayName("False: Given Example For Testing")
|
||||||
|
void isCloseTo_shouldReturnFalse_givenExample() {
|
||||||
|
var p1 = new LngLatDto(-3.192473, 55.946233);
|
||||||
|
var p2 = new LngLatDto(-3.192473, 55.942617);
|
||||||
|
boolean expected = false;
|
||||||
|
boolean actual = service.isCloseTo(p1, p2);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("True: Two points are the same")
|
||||||
|
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
|
||||||
|
var p1 = new LngLatDto(151.86, 285.37);
|
||||||
|
boolean expected = true;
|
||||||
|
boolean actual = service.isCloseTo(p1, p1);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("True: Two points are close to each other and near threshold")
|
||||||
|
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
|
||||||
|
var p1 = new LngLatDto(0.0, 0.0);
|
||||||
|
var p2 = new LngLatDto(0.0, 0.00014);
|
||||||
|
boolean expected = true;
|
||||||
|
boolean actual = service.isCloseTo(p1, p2);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("False: Distance nears the threshold")
|
||||||
|
void isCloseTo_shouldReturnFalse_whenEqualsToThreshold() {
|
||||||
|
var p1 = new LngLatDto(0.0, 0.0);
|
||||||
|
var p2 = new LngLatDto(0.0, 0.00015);
|
||||||
|
boolean expected = false;
|
||||||
|
boolean actual = service.isCloseTo(p1, p2);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("False: Distance larger to threshold")
|
||||||
|
void isCloseTo_shouldReturnFalse_whenNotCloseAndLargerThanThreshold() {
|
||||||
|
var p1 = new LngLatDto(0.0, 0.0);
|
||||||
|
var p2 = new LngLatDto(0.0, 0.00016);
|
||||||
|
boolean expected = false;
|
||||||
|
boolean actual = service.isCloseTo(p1, p2);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Test for nextPosition(LngLatDto, double) -> LngLatDto")
|
||||||
|
class NextPositionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("General Case: nextPosition in East direction (0 degrees)")
|
||||||
|
void nextPosition_shouldMoveEast_forAngleZero() {
|
||||||
|
var start = new LngLatDto(0.0, 0.0);
|
||||||
|
double angle = 0;
|
||||||
|
// For 0 degrees, cos(0)=1, sin(0)=0. Move happens entirely on lng axis.
|
||||||
|
var expected = new LngLatDto(STEP, 0.0);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
|
||||||
|
void nextPosition_shouldMoveNorth_forAngle90() {
|
||||||
|
var start = new LngLatDto(0.0, 0.0);
|
||||||
|
double angle = 90;
|
||||||
|
// For 90 degrees, cos(90)=0, sin(90)=1. Move happens entirely on lat axis.
|
||||||
|
var expected = new LngLatDto(0.0, STEP);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
|
||||||
|
void nextPosition_shouldMoveWest_forAngle180() {
|
||||||
|
var start = new LngLatDto(0.0, 0.0);
|
||||||
|
double angle = 180;
|
||||||
|
// For 180 degrees, cos(180)=-1, sin(180)=0. Move happens entirely on negative lng axis.
|
||||||
|
var expected = new LngLatDto(-STEP, 0.0);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
|
||||||
|
void nextPosition_shouldMoveSouth_forAngle270() {
|
||||||
|
var start = new LngLatDto(0.0, 0.0);
|
||||||
|
double angle = 270;
|
||||||
|
// For 270 degrees, cos(270)=0, sin(270)=-1. Move happens entirely on negative lat axis.
|
||||||
|
var expected = new LngLatDto(0.0, -STEP);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
|
||||||
|
void nextPosition_shouldMoveNortheast_forAngle45() {
|
||||||
|
var start = new LngLatDto(0.0, 0.0);
|
||||||
|
double 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));
|
||||||
|
var expected = new LngLatDto(expectedLng, expectedLat);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Edge Case: Angle larger than 360 should wrap around")
|
||||||
|
void nextPosition_shouldHandleAngleGreaterThan360() {
|
||||||
|
var start = new LngLatDto(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 LngLatDto(expectedLng, expectedLat);
|
||||||
|
|
||||||
|
var actual = service.nextPosition(start, angle);
|
||||||
|
|
||||||
|
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
|
||||||
|
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Edge Case: Negative angle should work correctly")
|
||||||
|
void nextPosition_shouldHandleNegativeAngle() {
|
||||||
|
var start = new LngLatDto(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 LngLatDto(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 RegionDto RECTANGLE_REGION = new RegionDto("rectangle", List.of(new LngLatDto(0.0, 0.0), new LngLatDto(2.0, 0.0), new LngLatDto(2.0, 2.0), new LngLatDto(0.0, 2.0), new LngLatDto(0.0, 0.0)));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("General Case: Given Example for Testing")
|
||||||
|
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
|
||||||
|
var position = new LngLatDto(1.234, 1.222);
|
||||||
|
var region = new RegionDto("central", List.of(new LngLatDto(-3.192473, 55.946233), new LngLatDto(-3.192473, 55.942617), new LngLatDto(-3.184319, 55.942617), new LngLatDto(-3.184319, 55.946233), new LngLatDto(-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 LngLatDto(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 LngLatDto(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 LngLatDto(2.0, 2.0);
|
||||||
|
var region = new RegionDto("hexagon", List.of(new LngLatDto(1.0, 0.0), new LngLatDto(4.0, 0.0), new LngLatDto(5.0, 2.0), new LngLatDto(4.0, 4.0), new LngLatDto(1.0, 4.0), new LngLatDto(0.0, 2.0), new LngLatDto(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 LngLatDto(0.00001, 0.00001);
|
||||||
|
var region = new RegionDto("triangle", List.of(new LngLatDto(0.0, 0.0), new LngLatDto(0.0001, 0.0), new LngLatDto(0.00005, 0.0001), new LngLatDto(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 LngLatDto(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 LngLatDto(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 LngLatDto(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 LngLatDto(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 LngLatDto(2.0, 2.0);
|
||||||
|
boolean expected = true;
|
||||||
|
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue