refractor(dto): Use nested data package

This commit is contained in:
js0ny 2025-11-24 00:35:29 +00:00
parent ec0d9087dd
commit 69d9e0d736
29 changed files with 405 additions and 383 deletions

View file

@ -6,11 +6,11 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.github.js0ny.ilp_coursework.data.DistanceRequestDto;
import io.github.js0ny.ilp_coursework.data.LngLatDto;
import io.github.js0ny.ilp_coursework.data.MovementRequestDto;
import io.github.js0ny.ilp_coursework.data.RegionCheckRequestDto;
import io.github.js0ny.ilp_coursework.data.RegionDto;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
/**
@ -48,39 +48,39 @@ public class ApiController {
/**
* Handles POST requests to get the distance between two positions
*
* @param request A {@link DistanceRequestDto} containing the two coordinates
* @param request A {@link DistanceRequest} containing the two coordinates
* @return A {@code double} representing the calculated distance
*/
@PostMapping("/distanceTo")
public double getDistance(@RequestBody DistanceRequestDto request) {
public double getDistance(@RequestBody DistanceRequest request) {
LngLatDto position1 = request.position1();
LngLatDto position2 = request.position2();
LngLat position1 = request.position1();
LngLat position2 = request.position2();
return gpsService.calculateDistance(position1, position2);
}
/**
* Handles POST requests to check if the two coordinates are close to each other
*
* @param request A {@link DistanceRequestDto} containing the two coordinates
* @param request A {@link DistanceRequest} containing the two coordinates
* @return {@code true} if the distance is less than the predefined threshold, {@code false} otherwise
*/
@PostMapping("/isCloseTo")
public boolean getIsCloseTo(@RequestBody DistanceRequestDto request) {
LngLatDto position1 = request.position1();
LngLatDto position2 = request.position2();
public boolean getIsCloseTo(@RequestBody DistanceRequest request) {
LngLat position1 = request.position1();
LngLat position2 = request.position2();
return gpsService.isCloseTo(position1, position2);
}
/**
* Handles POST requests to get the next position after an angle of movement
*
* @param request A {@link MovementRequestDto} containing the start coordinate and angle of the movement.
* @return A {@link LngLatDto} representing the destination
* @param request A {@link MovementRequest} containing the start coordinate and angle of the movement.
* @return A {@link LngLat} representing the destination
*/
@PostMapping("/nextPosition")
public LngLatDto getNextPosition(@RequestBody MovementRequestDto request) {
LngLatDto start = request.start();
public LngLat getNextPosition(@RequestBody MovementRequest request) {
LngLat start = request.start();
double angle = request.angle();
return gpsService.nextPosition(start, angle);
}
@ -88,13 +88,13 @@ public class ApiController {
/**
* Handles POST requests to check if a point is inside a given region
*
* @param request A {@link RegionCheckRequestDto} containing the coordinate and the region
* @param request A {@link RegionCheckRequest} containing the coordinate and the region
* @return {@code true} if the coordinate is inside the region, {@code false} otherwise
*/
@PostMapping("/isInRegion")
public boolean getIsInRegion(@RequestBody RegionCheckRequestDto request) {
LngLatDto position = request.position();
RegionDto region = request.region();
public boolean getIsInRegion(@RequestBody RegionCheckRequest request) {
LngLat position = request.position();
Region region = request.region();
return gpsService.checkIsInRegion(position, region);
}
}

View file

@ -1,10 +1,10 @@
package io.github.js0ny.ilp_coursework.controller;
import io.github.js0ny.ilp_coursework.data.AttrComparatorDto;
import io.github.js0ny.ilp_coursework.data.DeliveryPathDto;
import io.github.js0ny.ilp_coursework.data.DroneDto;
import io.github.js0ny.ilp_coursework.data.DronePathDto;
import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -55,13 +55,13 @@ public class DroneController {
* Handles GET requests to retrieve the drone detail identified by id
*
* @param id The id of the drone to be queried.
* @return 200 with {@link DroneDto}-style json if success, 404 if {@code id}
* @return 200 with {@link Drone}-style json if success, 404 if {@code id}
* not found, 400 otherwise
*/
@GetMapping("/droneDetails/{id}")
public ResponseEntity<DroneDto> getDroneDetail(@PathVariable String id) {
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
try {
DroneDto drone = droneService.droneDetail(id);
Drone drone = droneService.droneDetail(id);
return ResponseEntity.ok(drone);
} catch (IllegalArgumentException ex) {
return ResponseEntity.notFound().build();
@ -84,22 +84,22 @@ public class DroneController {
}
@PostMapping("/query")
public String[] getIdByAttrMapPost(@RequestBody AttrComparatorDto[] attrComparators) {
public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
return droneService.dronesSatisfyingAttributes(attrComparators);
}
@PostMapping("/queryAvailableDrones")
public String[] queryAvailableDrones(@RequestBody MedDispatchRecDto[] records) {
public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
return droneService.dronesMatchesRequirements(records);
}
@PostMapping("/calcDeliveryPath")
public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispatchRecDto[] record) {
return new DeliveryPathDto(0.0f, 0, new DronePathDto[]{});
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{});
}
@PostMapping("/calcDeliveryPathAsGeoJson")
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecDto[] record) {
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
return "{}";
}

View file

@ -1,7 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
// TODO: Convert operator to Enum
// import io.github.js0ny.ilp_coursework.util.AttrOperator;
public record AttrComparatorDto(String attribute, String operator, String value) {
}

View file

@ -1,7 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
public record DeliveryPathDto(
float totalCost,
int totalMoves,
DronePathDto[] dronePaths) {
}

View file

@ -1,13 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
/**
* Represents the data transfer object for a distance operation request.
* <p>
* This record encapsulates the data for several endpoints that involves two {@code LngLatDto}
* and serves as the data contract for those API operation
*
* @param position1 Nested object of {@link LngLatDto}
* @param position2 Nested object of {@link LngLatDto}
*/
public record DistanceRequestDto(LngLatDto position1, LngLatDto position2) {
}

View file

@ -1,10 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
/**
* Represents the data transfer object for a drone, gained from the endpoints
*/
public record DroneDto(
String name,
String id,
DroneCapabilityDto capability) {
}

View file

@ -1,4 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
public record DronePathDto() {
}

View file

@ -1,12 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
import java.time.LocalDate;
import java.time.LocalTime;
public record MedDispatchRecDto(
int id,
LocalDate date,
LocalTime time,
MedRequirementDto requirements,
LngLatDto delivery) {
}

View file

@ -1,8 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
public record MedRequirementDto(
float capacity,
boolean cooling,
boolean heating,
float maxCost
) {}

View file

@ -1,18 +0,0 @@
package io.github.js0ny.ilp_coursework.data;
import org.springframework.lang.Nullable;
public record ServicePointDronesDto(
String servicePointId,
DroneAvailabilityDto[] drones) {
@Nullable
public DroneAvailabilityDto locateDroneById(String droneId) {
for (var drone : drones) {
if (drone.id().equals(droneId)) {
return drone;
}
}
return null;
}
}

View file

@ -1,11 +1,11 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.common;
import java.time.DayOfWeek;
import java.time.LocalTime;
public record DroneAvailabilityDto(
public record DroneAvailability(
String id,
AvailabilityTimeSegDto[] availability) {
TimeWindow[] availability) {
public boolean checkAvailability(DayOfWeek day, LocalTime time) {

View file

@ -1,6 +1,6 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.common;
public record DroneCapabilityDto(
public record DroneCapability(
boolean cooling,
boolean heating,
float capacity,

View file

@ -0,0 +1,4 @@
package io.github.js0ny.ilp_coursework.data.common;
public record DronePathDto() {
}

View file

@ -1,4 +1,4 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.common;
/**
* Represents the data transfer object for a point or coordinate
@ -7,5 +7,5 @@ package io.github.js0ny.ilp_coursework.data;
* @param lng longitude of the coordinate/point
* @param lat latitude of the coordinate/point
*/
public record LngLatDto(double lng, double lat) {
public record LngLat(double lng, double lat) {
}

View file

@ -0,0 +1,9 @@
package io.github.js0ny.ilp_coursework.data.common;
public record MedRequirement(
float capacity,
boolean cooling,
boolean heating,
float maxCost
) {
}

View file

@ -1,4 +1,6 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.common;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import java.util.List;
import java.util.Objects;
@ -18,11 +20,11 @@ import java.util.Objects;
* In order to define a valid region, the last element of the
* list should be the same as the first, or
* known as closed
* @see RegionCheckRequestDto
* @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLatDto,
* RegionDto)
* @see RegionCheckRequest
* @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat,
* Region)
*/
public record RegionDto(String name, List<LngLatDto> vertices) {
public record Region(String name, List<LngLat> vertices) {
/**
* Magic number 4: For a polygon, 3 edges is required.
* <p>
@ -35,14 +37,14 @@ public record RegionDto(String name, List<LngLatDto> vertices) {
* {@code vertices} forms a closed polygon
*
* @return {@code true} if the {@code vertices} are able to form a polygon and
* form a closed polygon
* form a closed polygon
*/
public boolean isClosed() {
if (vertices == null || vertices.size() < MINIMUM_VERTICES) {
return false;
}
LngLatDto first = vertices.getFirst();
LngLatDto last = vertices.getLast();
LngLat first = vertices.getFirst();
LngLat last = vertices.getLast();
return Objects.equals(last, first);
}

View file

@ -1,9 +1,9 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.common;
import java.time.DayOfWeek;
import java.time.LocalTime;
public record AvailabilityTimeSegDto(
public record TimeWindow(
DayOfWeek dayOfWeek,
LocalTime from,
LocalTime until) {

View file

@ -0,0 +1,12 @@
package io.github.js0ny.ilp_coursework.data.external;
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
/**
* Represents the data transfer object for a drone, gained from the endpoints
*/
public record Drone(
String name,
String id,
DroneCapability capability) {
}

View file

@ -0,0 +1,19 @@
package io.github.js0ny.ilp_coursework.data.external;
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
import org.springframework.lang.Nullable;
public record ServicePointDrones(
String servicePointId,
DroneAvailability[] drones) {
@Nullable
public DroneAvailability locateDroneById(String droneId) {
for (var drone : drones) {
if (drone.id().equals(droneId)) {
return drone;
}
}
return null;
}
}

View file

@ -0,0 +1,7 @@
package io.github.js0ny.ilp_coursework.data.request;
// TODO: Convert operator to Enum
// import io.github.js0ny.ilp_coursework.util.AttrOperator;
public record AttrQueryRequest(String attribute, String operator, String value) {
}

View file

@ -0,0 +1,15 @@
package io.github.js0ny.ilp_coursework.data.request;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
/**
* Represents the data transfer object for a distance operation request.
* <p>
* This record encapsulates the data for several endpoints that involves two {@code LngLatDto}
* and serves as the data contract for those API operation
*
* @param position1 Nested object of {@link LngLat}
* @param position2 Nested object of {@link LngLat}
*/
public record DistanceRequest(LngLat position1, LngLat position2) {
}

View file

@ -0,0 +1,15 @@
package io.github.js0ny.ilp_coursework.data.request;
import io.github.js0ny.ilp_coursework.data.common.MedRequirement;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import java.time.LocalDate;
import java.time.LocalTime;
public record MedDispatchRecRequest(
int id,
LocalDate date,
LocalTime time,
MedRequirement requirements,
LngLat delivery) {
}

View file

@ -1,4 +1,6 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.request;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
/**
* Represents the data transfer object for a movement action request.
@ -9,7 +11,7 @@ package io.github.js0ny.ilp_coursework.data;
* @param start The starting coordinate of the movement
* @param angle The angle to movement in degree. This corresponds to compass directions.
* For example: 0 for East, 90 for North
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequestDto)
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
*/
public record MovementRequestDto(LngLatDto start, double angle) {
public record MovementRequest(LngLat start, double angle) {
}

View file

@ -1,4 +1,7 @@
package io.github.js0ny.ilp_coursework.data;
package io.github.js0ny.ilp_coursework.data.request;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
/**
* Represents the data transfer object for a region check request.
@ -9,8 +12,8 @@ package io.github.js0ny.ilp_coursework.data;
*
* @param position The coordinate to be checked
* @param region The region for the check.
* This is a nested object represented by {@link RegionDto}
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequestDto)
* This is a nested object represented by {@link Region}
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
*/
public record RegionCheckRequestDto(LngLatDto position, RegionDto region) {
public record RegionCheckRequest(LngLat position, Region region) {
}

View file

@ -0,0 +1,9 @@
package io.github.js0ny.ilp_coursework.data.response;
import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
public record DeliveryPathResponse(
float totalCost,
int totalMoves,
DronePathDto[] dronePaths) {
}

View file

@ -1,10 +1,10 @@
package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.AttrComparatorDto;
import io.github.js0ny.ilp_coursework.data.DroneDto;
import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto;
import io.github.js0ny.ilp_coursework.data.ServicePointDronesDto;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.util.AttrOperator;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
@ -59,9 +59,9 @@ public class DroneInfoService {
*/
public String[] dronesWithCooling(boolean state) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject(
Drone[] drones = restTemplate.getForObject(
droneUrl,
DroneDto[].class);
Drone[].class);
if (drones == null) {
return new String[]{};
@ -69,12 +69,12 @@ public class DroneInfoService {
return Arrays.stream(drones)
.filter(drone -> drone.capability().cooling() == state)
.map(DroneDto::id)
.map(Drone::id)
.toArray(String[]::new);
}
/**
* Return a {@link DroneDto}-style json data structure with the given {@code id}
* Return a {@link Drone}-style json data structure with the given {@code id}
* <p>
* Associated service method with {@code /droneDetails/{id}}
*
@ -87,11 +87,11 @@ public class DroneInfoService {
* this should lead to a 404
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
*/
public DroneDto droneDetail(String id) {
public Drone droneDetail(String id) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject(
Drone[] drones = restTemplate.getForObject(
droneUrl,
DroneDto[].class);
Drone[].class);
if (drones == null) {
throw new NullPointerException("drone cannot be found");
@ -131,7 +131,7 @@ public class DroneInfoService {
* @param attrComparators The filter rule with Name, Value and Operator
* @return array of drone ids that matches all rules
*/
public String[] dronesSatisfyingAttributes(AttrComparatorDto[] attrComparators) {
public String[] dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) {
String attribute = comparator.attribute();
@ -170,9 +170,9 @@ public class DroneInfoService {
private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
// This is required to make sure the response is valid
DroneDto[] drones = restTemplate.getForObject(
Drone[] drones = restTemplate.getForObject(
droneUrl,
DroneDto[].class);
Drone[].class);
if (drones == null) {
return new String[]{};
@ -193,7 +193,7 @@ public class DroneInfoService {
return false;
}
})
.map(DroneDto::id)
.map(Drone::id)
.toArray(String[]::new);
}
@ -207,11 +207,11 @@ public class DroneInfoService {
* @return array of drone ids that match all the requirements
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
*/
public String[] dronesMatchesRequirements(MedDispatchRecDto[] rec) {
public String[] dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject(
Drone[] drones = restTemplate.getForObject(
droneUrl,
DroneDto[].class);
Drone[].class);
if (drones == null || rec == null || rec.length == 0) {
return new String[]{};
@ -226,7 +226,7 @@ public class DroneInfoService {
.filter(record -> record != null && record.requirements() != null)
// Every record must be met
.allMatch(record -> meetsRequirement(drone, record)))
.map(DroneDto::id)
.map(Drone::id)
.toArray(String[]::new);
}
@ -240,7 +240,7 @@ public class DroneInfoService {
* is invalid (capacity and id cannot be null
* in {@code MedDispathRecDto})
*/
private boolean meetsRequirement(DroneDto drone, MedDispatchRecDto record) {
private boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
var requirements = record.requirements();
if (requirements == null) {
throw new IllegalArgumentException("requirements cannot be null");
@ -284,11 +284,11 @@ public class DroneInfoService {
* time
* @return true if the drone is available, false otherwise
*/
private boolean checkAvailability(String droneId, MedDispatchRecDto record) {
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
ServicePointDronesDto[] servicePoints = restTemplate.getForObject(
ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl,
ServicePointDronesDto[].class);
ServicePointDrones[].class);
LocalDate requiredDate = record.date();
DayOfWeek requiredDay = requiredDate.getDayOfWeek();

View file

@ -1,16 +1,19 @@
package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.*;
import java.util.List;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import org.springframework.stereotype.Service;
/**
* Class that handles calculations about Coordinates
*
* @see LngLatDto
* @see RegionDto
* @see LngLat
* @see Region
*/
@Service
public class GpsCalculationService {
@ -18,30 +21,28 @@ public class GpsCalculationService {
/**
* Given step size
*
* @see #nextPosition(LngLatDto, double)
* @see #nextPosition(LngLat, double)
*/
private static final double STEP = 0.00015;
/**
* Given threshold to judge if two points are close to each other
*
* @see #isCloseTo(LngLatDto, LngLatDto)
* @see #isCloseTo(LngLat, LngLat)
*/
private static final double CLOSE_THRESHOLD = 0.00015;
/**
* Calculate the Euclidean distance between {@code position1} and
* {@code position2}, which are coordinates
* defined as {@link LngLatDto}
* defined as {@link LngLat}
*
* @param position1 The coordinate of the first position
*
* @param position2 The coordinate of the second position
* @return The Euclidean distance between {@code position1} and
* {@code position2}
* @see
* io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequestDto)
* {@code position2}
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest)
*/
public double calculateDistance(LngLatDto position1, LngLatDto position2) {
public double calculateDistance(LngLat position1, LngLat position2) {
double lngDistance = position2.lng() - position1.lng();
double latDistance = position2.lat() - position1.lat();
// Euclidean: \sqrt{a^2 + b^2}
@ -51,20 +52,19 @@ public class GpsCalculationService {
/**
* Check if {@code position1} and
* {@code position2} are close to each other, the threshold is < 0.00015
*
*
* <p>
* Note that = 0.00015 will be counted as not close to and will return {@code
* false}
*
* @param position1 The coordinate of the first position
*
* @param position2 The coordinate of the second position
* @return {@code true} if {@code position1} and
* {@code position2} are close to each other
* {@code position2} are close to each other
* @see #CLOSE_THRESHOLD
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsCloseTo(DistanceRequestDto)
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsCloseTo(DistanceRequest)
*/
public boolean isCloseTo(LngLatDto position1, LngLatDto position2) {
public boolean isCloseTo(LngLat position1, LngLat position2) {
double distance = calculateDistance(position1, position2);
return distance < CLOSE_THRESHOLD;
}
@ -75,17 +75,16 @@ public class GpsCalculationService {
* 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}
* @see #STEP
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequestDto)
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getNextPosition(MovementRequest)
*/
public LngLatDto nextPosition(LngLatDto start, double angle) {
public LngLat nextPosition(LngLat start, double angle) {
double rad = Math.toRadians(angle); // Convert to radian for Java triangle function calculation
double newLng = Math.cos(rad) * STEP + start.lng();
double newLat = Math.sin(rad) * STEP + start.lat();
return new LngLatDto(newLng, newLat);
return new LngLat(newLng, newLat);
}
/**
@ -93,14 +92,14 @@ public class GpsCalculationService {
* is inside the {@code region}, on edge and vertex is considered as inside.
*
* @param position The coordinate of the position.
* @param region A {@link RegionDto} that contains name and a list of
* @param region A {@link Region} that contains name and a list of
* {@code LngLatDto}
* @return {@code true} if {@code position} is inside the {@code region}.
* @throws IllegalArgumentException If {@code region} is not closed
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequestDto)
* @see RegionDto#isClosed()
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
* @see Region#isClosed()
*/
public boolean checkIsInRegion(LngLatDto position, RegionDto region) throws IllegalArgumentException {
public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException {
if (!region.isClosed()) { // call method from RegionDto to check if not closed
throw new IllegalArgumentException("Region is not closed.");
}
@ -115,16 +114,16 @@ public class GpsCalculationService {
* @param polygon The region that forms a polygon to check if {@code point}
* sits inside.
* @return If the {@code point} sits inside the {@code polygon} then
* return {@code true}
* @see #isPointOnEdge(LngLatDto, LngLatDto, LngLatDto)
* @see #checkIsInRegion(LngLatDto, RegionDto)
* return {@code true}
* @see #isPointOnEdge(LngLat, LngLat, LngLat)
* @see #checkIsInRegion(LngLat, Region)
*/
private boolean rayCasting(LngLatDto point, List<LngLatDto> polygon) {
private boolean rayCasting(LngLat point, List<LngLat> 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
LngLat a = polygon.get(i);
LngLat b = polygon.get((i + 1) % n); // Next vertex
if (isPointOnEdge(point, a, b)) {
return true;
@ -132,7 +131,7 @@ public class GpsCalculationService {
// Ensure that `a` is norther than `b`, in order to easy classification
if (a.lat() > b.lat()) {
LngLatDto temp = a;
LngLat temp = a;
a = b;
b = temp;
}
@ -165,13 +164,12 @@ public class GpsCalculationService {
* {@code a} and {@code b}
*
* @param p point to be checked on the edge
*
* @param a point that forms the edge
* @param b point that forms the edge
* @return {@code true} if {@code p} is on {@code ab}
* @see #rayCasting(LngLatDto, List)
* @see #rayCasting(LngLat, List)
*/
private boolean isPointOnEdge(LngLatDto p, LngLatDto a, LngLatDto b) {
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat 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());

View file

@ -8,9 +8,15 @@ 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.*;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest;
import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@ -55,22 +61,22 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /distanceTo -> 200 OK")
void getDistance_shouldReturn200AndDistance_whenCorrectInput()
throws Exception {
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);
LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2);
when(
service.calculateDistance(
any(LngLatDto.class),
any(LngLatDto.class)
)
service.calculateDistance(
any(LngLat.class),
any(LngLat.class)
)
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@ -82,23 +88,23 @@ public class ApiControllerTest {
void getDistance_shouldReturn400_whenMissingField() throws Exception {
String endpoint = "/api/v1/distanceTo";
String req = """
{
"position1": {
"lng": 3.0,
"lat": 4.0
{
"position1": {
"lng": 3.0,
"lat": 4.0
}
}
}
""";
""";
when(
service.calculateDistance(any(LngLatDto.class), isNull())
service.calculateDistance(any(LngLat.class), isNull())
).thenThrow(new NullPointerException());
mockMvc
.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(req)
)
.andExpect(status().isBadRequest());
.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(req)
)
.andExpect(status().isBadRequest());
}
}
@ -109,19 +115,19 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isCloseTo -> 200 OK")
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput()
throws Exception {
throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isCloseTo";
LngLatDto p1 = new LngLatDto(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0);
var req = new DistanceRequestDto(p1, p2);
LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2);
when(
service.isCloseTo(any(LngLatDto.class), any(LngLatDto.class))
service.isCloseTo(any(LngLat.class), any(LngLat.class))
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@ -131,19 +137,19 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed()
throws Exception {
throws Exception {
// json without a bracket
String malformedJson = """
{
"position1": { "lng": 0.0, "lat": 3.0 }
""";
{
"position1": { "lng": 0.0, "lat": 3.0 }
""";
mockMvc
.perform(
post("/api/v1/isCloseTo")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson)
)
.andExpect(status().isBadRequest());
.perform(
post("/api/v1/isCloseTo")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson)
)
.andExpect(status().isBadRequest());
}
}
@ -156,46 +162,46 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /nextPosition -> 200 OK")
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput()
throws Exception {
LngLatDto expected = new LngLatDto(0.00015, 0.0);
LngLatDto p = new LngLatDto(0, 0);
var req = new MovementRequestDto(p, 0);
throws Exception {
LngLat expected = new LngLat(0.00015, 0.0);
LngLat p = new LngLat(0, 0);
var req = new MovementRequest(p, 0);
when(
service.nextPosition(any(LngLatDto.class), anyDouble())
service.nextPosition(any(LngLat.class), anyDouble())
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
mock.andExpect(
content().json(objectMapper.writeValueAsString(expected))
content().json(objectMapper.writeValueAsString(expected))
);
}
@Test
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
void getNextPosition_shouldReturn400_whenKeyNameError()
throws Exception {
throws Exception {
// "position" should be "start"
String malformedJson = """
{
"position": { "lng": 0.0, "lat": 3.0 },
"angle": 180
}
""";
{
"position": { "lng": 0.0, "lat": 3.0 },
"angle": 180
}
""";
when(service.nextPosition(isNull(), anyDouble())).thenThrow(
new NullPointerException()
new NullPointerException()
);
mockMvc
.perform(
post("/api/v1/nextPosition")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson)
)
.andExpect(MockMvcResultMatchers.status().isBadRequest());
.perform(
post("/api/v1/nextPosition")
.contentType(MediaType.APPLICATION_JSON)
.content(malformedJson)
)
.andExpect(MockMvcResultMatchers.status().isBadRequest());
}
}
@ -206,31 +212,31 @@ public class ApiControllerTest {
@Test
@DisplayName("POST /isInRegion -> 200 OK")
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput()
throws Exception {
throws Exception {
boolean expected = false;
String endpoint = "/api/v1/isInRegion";
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)
)
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)
)
);
var req = new RegionCheckRequestDto(position, region);
var req = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLatDto.class),
any(RegionDto.class)
)
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
);
mock.andExpect(status().isOk());
@ -239,59 +245,59 @@ public class ApiControllerTest {
@Test
@DisplayName(
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
)
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments()
throws Exception {
var position = new LngLatDto(1, 1);
var region = new RegionDto("illegal", List.of());
var request = new RegionCheckRequestDto(position, region);
throws Exception {
var position = new LngLat(1, 1);
var region = new Region("illegal", List.of());
var request = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLatDto.class),
any(RegionDto.class)
)
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
.perform(
post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isBadRequest());
.perform(
post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isBadRequest());
}
@Test
@DisplayName(
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
)
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices()
throws Exception {
var position = new LngLatDto(1, 1);
var region = new RegionDto(
"illegal",
List.of(
new LngLatDto(1, 2),
new LngLatDto(3, 4),
new LngLatDto(5, 6),
new LngLatDto(7, 8),
new LngLatDto(9, 10)
)
throws Exception {
var position = new LngLat(1, 1);
var region = new Region(
"illegal",
List.of(
new LngLat(1, 2),
new LngLat(3, 4),
new LngLat(5, 6),
new LngLat(7, 8),
new LngLat(9, 10)
)
);
var request = new RegionCheckRequestDto(position, region);
var request = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLatDto.class),
any(RegionDto.class)
)
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
.perform(
post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isBadRequest());
.perform(
post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isBadRequest());
}
}
}

View file

@ -1,7 +1,7 @@
package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.LngLatDto;
import io.github.js0ny.ilp_coursework.data.RegionDto;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@ -32,8 +32,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(-3.192473, 55.946233);
var p2 = new LngLat(-3.192473, 55.942617);
double expected = 0.0036;
double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(1e-4));
@ -42,8 +42,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(0, 3.0);
var p2 = new LngLat(4.0, 0);
double expected = 5.0;
double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -52,7 +52,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Points are Identical")
void calculateDistance_shouldReturnZero_whenPointsAreIdentical() {
var p1 = new LngLatDto(123.85, 983.2119);
var p1 = new LngLat(123.85, 983.2119);
double expected = 0.0;
double actual = service.calculateDistance(p1, p1);
assertThat(actual).isEqualTo(expected);
@ -61,8 +61,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(123.85, 983.2119);
var p2 = new LngLat(133.85, 983.2119);
double expected = 10.0;
double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -71,8 +71,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(123.85, 983.2119);
var p2 = new LngLat(123.85, 973.2119);
double expected = 10.0;
double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -81,8 +81,8 @@ public class GpsCalculationServiceTest {
@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
LngLat p1 = new LngLat(-1.0, -2.0);
LngLat p2 = new LngLat(2.0, 2.0); // lngDiff = 3, latDiff = 4
double expected = 5.0;
double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -95,8 +95,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(-3.192473, 55.946233);
var p2 = new LngLat(-3.192473, 55.942617);
boolean expected = false;
boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected);
@ -105,7 +105,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("True: Two points are the same")
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
var p1 = new LngLatDto(151.86, 285.37);
var p1 = new LngLat(151.86, 285.37);
boolean expected = true;
boolean actual = service.isCloseTo(p1, p1);
assertThat(actual).isEqualTo(expected);
@ -114,8 +114,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00014);
boolean expected = true;
boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected);
@ -124,8 +124,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00015);
boolean expected = false;
boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected);
@ -134,8 +134,8 @@ public class GpsCalculationServiceTest {
@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);
var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00016);
boolean expected = false;
boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected);
@ -149,10 +149,10 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("General Case: nextPosition in East direction (0 degrees)")
void nextPosition_shouldMoveEast_forAngleZero() {
var start = new LngLatDto(0.0, 0.0);
var start = new LngLat(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 expected = new LngLat(STEP, 0.0);
var actual = service.nextPosition(start, angle);
@ -163,10 +163,10 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
void nextPosition_shouldMoveNorth_forAngle90() {
var start = new LngLatDto(0.0, 0.0);
var start = new LngLat(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 expected = new LngLat(0.0, STEP);
var actual = service.nextPosition(start, angle);
@ -177,11 +177,11 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
void nextPosition_shouldMoveWest_forAngle180() {
var start = new LngLatDto(0.0, 0.0);
var start = new LngLat(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 expected = new LngLat(-STEP, 0.0);
var actual = service.nextPosition(start, angle);
@ -192,11 +192,11 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
void nextPosition_shouldMoveSouth_forAngle270() {
var start = new LngLatDto(0.0, 0.0);
var start = new LngLat(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 expected = new LngLat(0.0, -STEP);
var actual = service.nextPosition(start, angle);
@ -207,12 +207,12 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
void nextPosition_shouldMoveNortheast_forAngle45() {
var start = new LngLatDto(0.0, 0.0);
var start = new LngLat(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 expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle);
@ -223,13 +223,13 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Angle larger than 360 should wrap around")
void nextPosition_shouldHandleAngleGreaterThan360() {
var start = new LngLatDto(0.0, 0.0);
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 LngLatDto(expectedLng, expectedLat);
var expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle);
@ -240,12 +240,12 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Negative angle should work correctly")
void nextPosition_shouldHandleNegativeAngle() {
var start = new LngLatDto(0.0, 0.0);
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 LngLatDto(expectedLng, expectedLat);
var expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle);
@ -258,17 +258,17 @@ public class GpsCalculationServiceTest {
@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)));
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 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)));
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);
@ -277,7 +277,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("General Case: Simple Rectangle")
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
var position = new LngLatDto(1.0, 1.0);
var position = new LngLat(1.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -286,7 +286,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("General Case: Simple Rectangle")
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
var position = new LngLatDto(3.0, 1.0);
var position = new LngLat(3.0, 1.0);
boolean expected = false;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -295,11 +295,11 @@ public class GpsCalculationServiceTest {
@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)));
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);
@ -308,9 +308,9 @@ public class GpsCalculationServiceTest {
@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)));
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);
@ -319,7 +319,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Point on Lower Edge of Rectangle")
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
var position = new LngLatDto(0.0, 1.0);
var position = new LngLat(0.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -328,7 +328,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Point on Upper Edge of Rectangle")
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
var position = new LngLatDto(2.0, 1.0);
var position = new LngLat(2.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -337,7 +337,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Point on Left Edge")
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
var position = new LngLatDto(0.0, 1.0);
var position = new LngLat(0.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -346,7 +346,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Point on Lower Vertex")
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
var position = new LngLatDto(0.0, 0.0);
var position = new LngLat(0.0, 0.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -355,7 +355,7 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Point on Upper Vertex")
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
var position = new LngLatDto(2.0, 2.0);
var position = new LngLat(2.0, 2.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected);
@ -364,9 +364,9 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Region not forming polygon")
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
var position = new LngLatDto(2.0, 2.0);
var region = new RegionDto("line",
List.of(new LngLatDto(0.0, 0.0), new LngLatDto(0.0001, 0.0), new LngLatDto(0.0, 0.0)));
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.");
@ -375,9 +375,9 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Region is not closed")
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
var position = new LngLatDto(2.0, 2.0);
var 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, -1.0)));
var position = new LngLat(2.0, 2.0);
var region = new Region("rectangle", List.of(new LngLat(0.0, 0.0), new LngLat(2.0, 0.0),
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.");
@ -386,8 +386,8 @@ public class GpsCalculationServiceTest {
@Test
@DisplayName("Edge Case: Vertex list is empty")
void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
var position = new LngLatDto(2.0, 2.0);
var region = new RegionDto("rectangle", List.of());
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.");