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

View file

@ -1,10 +1,10 @@
package io.github.js0ny.ilp_coursework.controller; package io.github.js0ny.ilp_coursework.controller;
import io.github.js0ny.ilp_coursework.data.AttrComparatorDto; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import io.github.js0ny.ilp_coursework.data.DeliveryPathDto; import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.DroneDto; import io.github.js0ny.ilp_coursework.data.common.DronePathDto;
import io.github.js0ny.ilp_coursework.data.DronePathDto; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto; import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.service.DroneInfoService; import io.github.js0ny.ilp_coursework.service.DroneInfoService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -55,13 +55,13 @@ public class DroneController {
* Handles GET requests to retrieve the drone detail identified by id * Handles GET requests to retrieve the drone detail identified by id
* *
* @param id The id of the drone to be queried. * @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 * not found, 400 otherwise
*/ */
@GetMapping("/droneDetails/{id}") @GetMapping("/droneDetails/{id}")
public ResponseEntity<DroneDto> getDroneDetail(@PathVariable String id) { public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
try { try {
DroneDto drone = droneService.droneDetail(id); Drone drone = droneService.droneDetail(id);
return ResponseEntity.ok(drone); return ResponseEntity.ok(drone);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@ -84,22 +84,22 @@ public class DroneController {
} }
@PostMapping("/query") @PostMapping("/query")
public String[] getIdByAttrMapPost(@RequestBody AttrComparatorDto[] attrComparators) { public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
return droneService.dronesSatisfyingAttributes(attrComparators); return droneService.dronesSatisfyingAttributes(attrComparators);
} }
@PostMapping("/queryAvailableDrones") @PostMapping("/queryAvailableDrones")
public String[] queryAvailableDrones(@RequestBody MedDispatchRecDto[] records) { public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
return droneService.dronesMatchesRequirements(records); return droneService.dronesMatchesRequirements(records);
} }
@PostMapping("/calcDeliveryPath") @PostMapping("/calcDeliveryPath")
public DeliveryPathDto calculateDeliveryPath(@RequestBody MedDispatchRecDto[] record) { public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
return new DeliveryPathDto(0.0f, 0, new DronePathDto[]{}); return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{});
} }
@PostMapping("/calcDeliveryPathAsGeoJson") @PostMapping("/calcDeliveryPathAsGeoJson")
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecDto[] record) { public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
return "{}"; 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.DayOfWeek;
import java.time.LocalTime; import java.time.LocalTime;
public record DroneAvailabilityDto( public record DroneAvailability(
String id, String id,
AvailabilityTimeSegDto[] availability) { TimeWindow[] availability) {
public boolean checkAvailability(DayOfWeek day, LocalTime time) { 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 cooling,
boolean heating, boolean heating,
float capacity, 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 * 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 lng longitude of the coordinate/point
* @param lat latitude 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.List;
import java.util.Objects; import java.util.Objects;
@ -18,11 +20,11 @@ import java.util.Objects;
* In order to define a valid region, the last element of the * In order to define a valid region, the last element of the
* list should be the same as the first, or * list should be the same as the first, or
* known as closed * known as closed
* @see RegionCheckRequestDto * @see RegionCheckRequest
* @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLatDto, * @see io.github.js0ny.ilp_coursework.service.GpsCalculationService#checkIsInRegion(LngLat,
* RegionDto) * 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. * Magic number 4: For a polygon, 3 edges is required.
* <p> * <p>
@ -35,14 +37,14 @@ public record RegionDto(String name, List<LngLatDto> vertices) {
* {@code vertices} forms a closed polygon * {@code vertices} forms a closed polygon
* *
* @return {@code true} if the {@code vertices} are able to form a polygon and * @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() { public boolean isClosed() {
if (vertices == null || vertices.size() < MINIMUM_VERTICES) { if (vertices == null || vertices.size() < MINIMUM_VERTICES) {
return false; return false;
} }
LngLatDto first = vertices.getFirst(); LngLat first = vertices.getFirst();
LngLatDto last = vertices.getLast(); LngLat last = vertices.getLast();
return Objects.equals(last, first); 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.DayOfWeek;
import java.time.LocalTime; import java.time.LocalTime;
public record AvailabilityTimeSegDto( public record TimeWindow(
DayOfWeek dayOfWeek, DayOfWeek dayOfWeek,
LocalTime from, LocalTime from,
LocalTime until) { 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. * 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 start The starting coordinate of the movement
* @param angle The angle to movement in degree. This corresponds to compass directions. * @param angle The angle to movement in degree. This corresponds to compass directions.
* For example: 0 for East, 90 for North * 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. * 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 position The coordinate to be checked
* @param region The region for the check. * @param region The region for the check.
* This is a nested object represented by {@link RegionDto} * This is a nested object represented by {@link Region}
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequestDto) * @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; package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.AttrComparatorDto; import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.DroneDto; import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.MedDispatchRecDto; import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.data.ServicePointDronesDto;
import io.github.js0ny.ilp_coursework.util.AttrOperator; import io.github.js0ny.ilp_coursework.util.AttrOperator;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched; import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
@ -59,9 +59,9 @@ public class DroneInfoService {
*/ */
public String[] dronesWithCooling(boolean state) { public String[] dronesWithCooling(boolean state) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject( Drone[] drones = restTemplate.getForObject(
droneUrl, droneUrl,
DroneDto[].class); Drone[].class);
if (drones == null) { if (drones == null) {
return new String[]{}; return new String[]{};
@ -69,12 +69,12 @@ public class DroneInfoService {
return Arrays.stream(drones) return Arrays.stream(drones)
.filter(drone -> drone.capability().cooling() == state) .filter(drone -> drone.capability().cooling() == state)
.map(DroneDto::id) .map(Drone::id)
.toArray(String[]::new); .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> * <p>
* Associated service method with {@code /droneDetails/{id}} * Associated service method with {@code /droneDetails/{id}}
* *
@ -87,11 +87,11 @@ public class DroneInfoService {
* this should lead to a 404 * this should lead to a 404
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String) * @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); URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject( Drone[] drones = restTemplate.getForObject(
droneUrl, droneUrl,
DroneDto[].class); Drone[].class);
if (drones == null) { if (drones == null) {
throw new NullPointerException("drone cannot be found"); throw new NullPointerException("drone cannot be found");
@ -131,7 +131,7 @@ public class DroneInfoService {
* @param attrComparators The filter rule with Name, Value and Operator * @param attrComparators The filter rule with Name, Value and Operator
* @return array of drone ids that matches all rules * @return array of drone ids that matches all rules
*/ */
public String[] dronesSatisfyingAttributes(AttrComparatorDto[] attrComparators) { public String[] dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
Set<String> matchingDroneIds = null; Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) { for (var comparator : attrComparators) {
String attribute = comparator.attribute(); String attribute = comparator.attribute();
@ -170,9 +170,9 @@ public class DroneInfoService {
private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) { private String[] dronesWithAttributeCompared(String attrName, String attrVal, AttrOperator op) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
// This is required to make sure the response is valid // This is required to make sure the response is valid
DroneDto[] drones = restTemplate.getForObject( Drone[] drones = restTemplate.getForObject(
droneUrl, droneUrl,
DroneDto[].class); Drone[].class);
if (drones == null) { if (drones == null) {
return new String[]{}; return new String[]{};
@ -193,7 +193,7 @@ public class DroneInfoService {
return false; return false;
} }
}) })
.map(DroneDto::id) .map(Drone::id)
.toArray(String[]::new); .toArray(String[]::new);
} }
@ -207,11 +207,11 @@ public class DroneInfoService {
* @return array of drone ids that match all the requirements * @return array of drone ids that match all the requirements
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones * @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
*/ */
public String[] dronesMatchesRequirements(MedDispatchRecDto[] rec) { public String[] dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
DroneDto[] drones = restTemplate.getForObject( Drone[] drones = restTemplate.getForObject(
droneUrl, droneUrl,
DroneDto[].class); Drone[].class);
if (drones == null || rec == null || rec.length == 0) { if (drones == null || rec == null || rec.length == 0) {
return new String[]{}; return new String[]{};
@ -226,7 +226,7 @@ public class DroneInfoService {
.filter(record -> record != null && record.requirements() != null) .filter(record -> record != null && record.requirements() != null)
// Every record must be met // Every record must be met
.allMatch(record -> meetsRequirement(drone, record))) .allMatch(record -> meetsRequirement(drone, record)))
.map(DroneDto::id) .map(Drone::id)
.toArray(String[]::new); .toArray(String[]::new);
} }
@ -240,7 +240,7 @@ public class DroneInfoService {
* is invalid (capacity and id cannot be null * is invalid (capacity and id cannot be null
* in {@code MedDispathRecDto}) * in {@code MedDispathRecDto})
*/ */
private boolean meetsRequirement(DroneDto drone, MedDispatchRecDto record) { private boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
var requirements = record.requirements(); var requirements = record.requirements();
if (requirements == null) { if (requirements == null) {
throw new IllegalArgumentException("requirements cannot be null"); throw new IllegalArgumentException("requirements cannot be null");
@ -284,11 +284,11 @@ public class DroneInfoService {
* time * time
* @return true if the drone is available, false otherwise * @return true if the drone is available, false otherwise
*/ */
private boolean checkAvailability(String droneId, MedDispatchRecDto record) { private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
ServicePointDronesDto[] servicePoints = restTemplate.getForObject( ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl, droneUrl,
ServicePointDronesDto[].class); ServicePointDrones[].class);
LocalDate requiredDate = record.date(); LocalDate requiredDate = record.date();
DayOfWeek requiredDay = requiredDate.getDayOfWeek(); DayOfWeek requiredDay = requiredDate.getDayOfWeek();

View file

@ -1,16 +1,19 @@
package io.github.js0ny.ilp_coursework.service; package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.*;
import java.util.List; 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; import org.springframework.stereotype.Service;
/** /**
* Class that handles calculations about Coordinates * Class that handles calculations about Coordinates
* *
* @see LngLatDto * @see LngLat
* @see RegionDto * @see Region
*/ */
@Service @Service
public class GpsCalculationService { public class GpsCalculationService {
@ -18,30 +21,28 @@ public class GpsCalculationService {
/** /**
* Given step size * Given step size
* *
* @see #nextPosition(LngLatDto, double) * @see #nextPosition(LngLat, double)
*/ */
private static final double STEP = 0.00015; private static final double STEP = 0.00015;
/** /**
* Given threshold to judge if two points are close to each other * 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; private static final double CLOSE_THRESHOLD = 0.00015;
/** /**
* Calculate the Euclidean distance between {@code position1} and * Calculate the Euclidean distance between {@code position1} and
* {@code position2}, which are coordinates * {@code position2}, which are coordinates
* defined as {@link LngLatDto} * defined as {@link LngLat}
* *
* @param position1 The coordinate of the first position * @param position1 The coordinate of the first position
*
* @param position2 The coordinate of the second position * @param position2 The coordinate of the second position
* @return The Euclidean distance between {@code position1} and * @return The Euclidean distance between {@code position1} and
* {@code position2} * {@code position2}
* @see * @see io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequest)
* io.github.js0ny.ilp_coursework.controller.ApiController#getDistance(DistanceRequestDto)
*/ */
public double calculateDistance(LngLatDto position1, LngLatDto position2) { public double calculateDistance(LngLat position1, LngLat position2) {
double lngDistance = position2.lng() - position1.lng(); double lngDistance = position2.lng() - position1.lng();
double latDistance = position2.lat() - position1.lat(); double latDistance = position2.lat() - position1.lat();
// Euclidean: \sqrt{a^2 + b^2} // Euclidean: \sqrt{a^2 + b^2}
@ -51,20 +52,19 @@ public class GpsCalculationService {
/** /**
* Check if {@code position1} and * Check if {@code position1} and
* {@code position2} are close to each other, the threshold is < 0.00015 * {@code position2} are close to each other, the threshold is < 0.00015
* *
* <p> * <p>
* Note that = 0.00015 will be counted as not close to and will return {@code * Note that = 0.00015 will be counted as not close to and will return {@code
* false} * false}
* *
* @param position1 The coordinate of the first position * @param position1 The coordinate of the first position
*
* @param position2 The coordinate of the second position * @param position2 The coordinate of the second position
* @return {@code true} if {@code position1} and * @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 #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); double distance = calculateDistance(position1, position2);
return distance < CLOSE_THRESHOLD; return distance < CLOSE_THRESHOLD;
} }
@ -75,17 +75,16 @@ public class GpsCalculationService {
* 0.00015 * 0.00015
* *
* @param start The coordinate of the original start point. * @param start The coordinate of the original start point.
*
* @param angle The direction to be moved in angle. * @param angle The direction to be moved in angle.
* @return The next position moved from {@code start} * @return The next position moved from {@code start}
* @see #STEP * @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 rad = Math.toRadians(angle); // Convert to radian for Java triangle function calculation
double newLng = Math.cos(rad) * STEP + start.lng(); double newLng = Math.cos(rad) * STEP + start.lng();
double newLat = Math.sin(rad) * STEP + start.lat(); 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. * is inside the {@code region}, on edge and vertex is considered as inside.
* *
* @param position The coordinate of the position. * @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} * {@code LngLatDto}
* @return {@code true} if {@code position} is inside the {@code region}. * @return {@code true} if {@code position} is inside the {@code region}.
* @throws IllegalArgumentException If {@code region} is not closed * @throws IllegalArgumentException If {@code region} is not closed
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequestDto) * @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
* @see RegionDto#isClosed() * @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 if (!region.isClosed()) { // call method from RegionDto to check if not closed
throw new IllegalArgumentException("Region is not closed."); throw new IllegalArgumentException("Region is not closed.");
} }
@ -115,16 +114,16 @@ public class GpsCalculationService {
* @param polygon The region that forms a polygon to check if {@code point} * @param polygon The region that forms a polygon to check if {@code point}
* sits inside. * sits inside.
* @return If the {@code point} sits inside the {@code polygon} then * @return If the {@code point} sits inside the {@code polygon} then
* return {@code true} * return {@code true}
* @see #isPointOnEdge(LngLatDto, LngLatDto, LngLatDto) * @see #isPointOnEdge(LngLat, LngLat, LngLat)
* @see #checkIsInRegion(LngLatDto, RegionDto) * @see #checkIsInRegion(LngLat, Region)
*/ */
private boolean rayCasting(LngLatDto point, List<LngLatDto> polygon) { private boolean rayCasting(LngLat point, List<LngLat> polygon) {
int intersections = 0; int intersections = 0;
int n = polygon.size(); int n = polygon.size();
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
LngLatDto a = polygon.get(i); LngLat a = polygon.get(i);
LngLatDto b = polygon.get((i + 1) % n); // Next vertex LngLat b = polygon.get((i + 1) % n); // Next vertex
if (isPointOnEdge(point, a, b)) { if (isPointOnEdge(point, a, b)) {
return true; return true;
@ -132,7 +131,7 @@ public class GpsCalculationService {
// Ensure that `a` is norther than `b`, in order to easy classification // Ensure that `a` is norther than `b`, in order to easy classification
if (a.lat() > b.lat()) { if (a.lat() > b.lat()) {
LngLatDto temp = a; LngLat temp = a;
a = b; a = b;
b = temp; b = temp;
} }
@ -165,13 +164,12 @@ public class GpsCalculationService {
* {@code a} and {@code b} * {@code a} and {@code b}
* *
* @param p point to be checked on the edge * @param p point to be checked on the edge
*
* @param a point that forms the edge * @param a point that forms the edge
* @param b point that forms the edge * @param b point that forms the edge
* @return {@code true} if {@code p} is on {@code ab} * @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) // Cross product: (p - a) × (b - a)
double crossProduct = (p.lng() - a.lng()) * (b.lat() - a.lat()) double crossProduct = (p.lng() - a.lng()) * (b.lat() - a.lat())
- (p.lat() - a.lat()) * (b.lng() - a.lng()); - (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 static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper; 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 io.github.js0ny.ilp_coursework.service.GpsCalculationService;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -55,22 +61,22 @@ public class ApiControllerTest {
@Test @Test
@DisplayName("POST /distanceTo -> 200 OK") @DisplayName("POST /distanceTo -> 200 OK")
void getDistance_shouldReturn200AndDistance_whenCorrectInput() void getDistance_shouldReturn200AndDistance_whenCorrectInput()
throws Exception { throws Exception {
double expected = 5.0; double expected = 5.0;
String endpoint = "/api/v1/distanceTo"; String endpoint = "/api/v1/distanceTo";
LngLatDto p1 = new LngLatDto(0, 4.0); LngLat p1 = new LngLat(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0); LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequestDto(p1, p2); var req = new DistanceRequest(p1, p2);
when( when(
service.calculateDistance( service.calculateDistance(
any(LngLatDto.class), any(LngLat.class),
any(LngLatDto.class) any(LngLat.class)
) )
).thenReturn(expected); ).thenReturn(expected);
var mock = mockMvc.perform( var mock = mockMvc.perform(
post(endpoint) post(endpoint)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)) .content(objectMapper.writeValueAsString(req))
); );
mock.andExpect(status().isOk()); mock.andExpect(status().isOk());
@ -82,23 +88,23 @@ public class ApiControllerTest {
void getDistance_shouldReturn400_whenMissingField() throws Exception { void getDistance_shouldReturn400_whenMissingField() throws Exception {
String endpoint = "/api/v1/distanceTo"; String endpoint = "/api/v1/distanceTo";
String req = """ String req = """
{ {
"position1": { "position1": {
"lng": 3.0, "lng": 3.0,
"lat": 4.0 "lat": 4.0
}
} }
} """;
""";
when( when(
service.calculateDistance(any(LngLatDto.class), isNull()) service.calculateDistance(any(LngLat.class), isNull())
).thenThrow(new NullPointerException()); ).thenThrow(new NullPointerException());
mockMvc mockMvc
.perform( .perform(
post(endpoint) post(endpoint)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(req) .content(req)
) )
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
} }
} }
@ -109,19 +115,19 @@ public class ApiControllerTest {
@Test @Test
@DisplayName("POST /isCloseTo -> 200 OK") @DisplayName("POST /isCloseTo -> 200 OK")
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput() void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput()
throws Exception { throws Exception {
boolean expected = false; boolean expected = false;
String endpoint = "/api/v1/isCloseTo"; String endpoint = "/api/v1/isCloseTo";
LngLatDto p1 = new LngLatDto(0, 4.0); LngLat p1 = new LngLat(0, 4.0);
LngLatDto p2 = new LngLatDto(3.0, 0); LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequestDto(p1, p2); var req = new DistanceRequest(p1, p2);
when( when(
service.isCloseTo(any(LngLatDto.class), any(LngLatDto.class)) service.isCloseTo(any(LngLat.class), any(LngLat.class))
).thenReturn(expected); ).thenReturn(expected);
var mock = mockMvc.perform( var mock = mockMvc.perform(
post(endpoint) post(endpoint)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)) .content(objectMapper.writeValueAsString(req))
); );
mock.andExpect(status().isOk()); mock.andExpect(status().isOk());
@ -131,19 +137,19 @@ public class ApiControllerTest {
@Test @Test
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ") @DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed() void getIsCloseTo_shouldReturn400_whenJsonIsMalformed()
throws Exception { throws Exception {
// json without a bracket // json without a bracket
String malformedJson = """ String malformedJson = """
{ {
"position1": { "lng": 0.0, "lat": 3.0 } "position1": { "lng": 0.0, "lat": 3.0 }
"""; """;
mockMvc mockMvc
.perform( .perform(
post("/api/v1/isCloseTo") post("/api/v1/isCloseTo")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(malformedJson) .content(malformedJson)
) )
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
} }
} }
@ -156,46 +162,46 @@ public class ApiControllerTest {
@Test @Test
@DisplayName("POST /nextPosition -> 200 OK") @DisplayName("POST /nextPosition -> 200 OK")
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput() void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput()
throws Exception { throws Exception {
LngLatDto expected = new LngLatDto(0.00015, 0.0); LngLat expected = new LngLat(0.00015, 0.0);
LngLatDto p = new LngLatDto(0, 0); LngLat p = new LngLat(0, 0);
var req = new MovementRequestDto(p, 0); var req = new MovementRequest(p, 0);
when( when(
service.nextPosition(any(LngLatDto.class), anyDouble()) service.nextPosition(any(LngLat.class), anyDouble())
).thenReturn(expected); ).thenReturn(expected);
var mock = mockMvc.perform( var mock = mockMvc.perform(
post(endpoint) post(endpoint)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)) .content(objectMapper.writeValueAsString(req))
); );
mock.andExpect(status().isOk()); mock.andExpect(status().isOk());
mock.andExpect( mock.andExpect(
content().json(objectMapper.writeValueAsString(expected)) content().json(objectMapper.writeValueAsString(expected))
); );
} }
@Test @Test
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field") @DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
void getNextPosition_shouldReturn400_whenKeyNameError() void getNextPosition_shouldReturn400_whenKeyNameError()
throws Exception { throws Exception {
// "position" should be "start" // "position" should be "start"
String malformedJson = """ String malformedJson = """
{ {
"position": { "lng": 0.0, "lat": 3.0 }, "position": { "lng": 0.0, "lat": 3.0 },
"angle": 180 "angle": 180
} }
"""; """;
when(service.nextPosition(isNull(), anyDouble())).thenThrow( when(service.nextPosition(isNull(), anyDouble())).thenThrow(
new NullPointerException() new NullPointerException()
); );
mockMvc mockMvc
.perform( .perform(
post("/api/v1/nextPosition") post("/api/v1/nextPosition")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(malformedJson) .content(malformedJson)
) )
.andExpect(MockMvcResultMatchers.status().isBadRequest()); .andExpect(MockMvcResultMatchers.status().isBadRequest());
} }
} }
@ -206,31 +212,31 @@ public class ApiControllerTest {
@Test @Test
@DisplayName("POST /isInRegion -> 200 OK") @DisplayName("POST /isInRegion -> 200 OK")
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput() void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput()
throws Exception { throws Exception {
boolean expected = false; boolean expected = false;
String endpoint = "/api/v1/isInRegion"; String endpoint = "/api/v1/isInRegion";
var position = new LngLatDto(1.234, 1.222); var position = new LngLat(1.234, 1.222);
var region = new RegionDto( var region = new Region(
"central", "central",
List.of( List.of(
new LngLatDto(-3.192473, 55.946233), new LngLat(-3.192473, 55.946233),
new LngLatDto(-3.192473, 55.942617), new LngLat(-3.192473, 55.942617),
new LngLatDto(-3.184319, 55.942617), new LngLat(-3.184319, 55.942617),
new LngLatDto(-3.184319, 55.946233), new LngLat(-3.184319, 55.946233),
new LngLatDto(-3.192473, 55.946233) new LngLat(-3.192473, 55.946233)
) )
); );
var req = new RegionCheckRequestDto(position, region); var req = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(
any(LngLatDto.class), any(LngLat.class),
any(RegionDto.class) any(Region.class)
) )
).thenReturn(expected); ).thenReturn(expected);
var mock = mockMvc.perform( var mock = mockMvc.perform(
post(endpoint) post(endpoint)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)) .content(objectMapper.writeValueAsString(req))
); );
mock.andExpect(status().isOk()); mock.andExpect(status().isOk());
@ -239,59 +245,59 @@ public class ApiControllerTest {
@Test @Test
@DisplayName( @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() void getIsInRegion_shouldReturn400_whenPassingIllegalArguments()
throws Exception { throws Exception {
var position = new LngLatDto(1, 1); var position = new LngLat(1, 1);
var region = new RegionDto("illegal", List.of()); var region = new Region("illegal", List.of());
var request = new RegionCheckRequestDto(position, region); var request = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(
any(LngLatDto.class), any(LngLat.class),
any(RegionDto.class) any(Region.class)
) )
).thenThrow(new IllegalArgumentException("Region is not closed.")); ).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc mockMvc
.perform( .perform(
post("/api/v1/isInRegion") post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)) .content(objectMapper.writeValueAsString(request))
) )
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
} }
@Test @Test
@DisplayName( @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() void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices()
throws Exception { throws Exception {
var position = new LngLatDto(1, 1); var position = new LngLat(1, 1);
var region = new RegionDto( var region = new Region(
"illegal", "illegal",
List.of( List.of(
new LngLatDto(1, 2), new LngLat(1, 2),
new LngLatDto(3, 4), new LngLat(3, 4),
new LngLatDto(5, 6), new LngLat(5, 6),
new LngLatDto(7, 8), new LngLat(7, 8),
new LngLatDto(9, 10) new LngLat(9, 10)
) )
); );
var request = new RegionCheckRequestDto(position, region); var request = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(
any(LngLatDto.class), any(LngLat.class),
any(RegionDto.class) any(Region.class)
) )
).thenThrow(new IllegalArgumentException("Region is not closed.")); ).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc mockMvc
.perform( .perform(
post("/api/v1/isInRegion") post("/api/v1/isInRegion")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)) .content(objectMapper.writeValueAsString(request))
) )
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
} }
} }
} }

View file

@ -1,7 +1,7 @@
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.common.LngLat;
import io.github.js0ny.ilp_coursework.data.RegionDto; import io.github.js0ny.ilp_coursework.data.common.Region;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
@ -32,8 +32,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("False: Given Example For Testing") @DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() { void isCloseTo_shouldReturnFalse_givenExample() {
var p1 = new LngLatDto(-3.192473, 55.946233); var p1 = new LngLat(-3.192473, 55.946233);
var p2 = new LngLatDto(-3.192473, 55.942617); var p2 = new LngLat(-3.192473, 55.942617);
double expected = 0.0036; double expected = 0.0036;
double actual = service.calculateDistance(p1, p2); double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(1e-4)); assertThat(actual).isCloseTo(expected, within(1e-4));
@ -42,8 +42,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: 3-4-5 Triangle") @DisplayName("General Case: 3-4-5 Triangle")
void calculateDistance_shouldReturnCorrectEuclideanDistance_forGeneralCase() { void calculateDistance_shouldReturnCorrectEuclideanDistance_forGeneralCase() {
var p1 = new LngLatDto(0, 3.0); var p1 = new LngLat(0, 3.0);
var p2 = new LngLatDto(4.0, 0); var p2 = new LngLat(4.0, 0);
double expected = 5.0; double expected = 5.0;
double actual = service.calculateDistance(p1, p2); double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION)); assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -52,7 +52,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Points are Identical") @DisplayName("Edge Case: Points are Identical")
void calculateDistance_shouldReturnZero_whenPointsAreIdentical() { 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 expected = 0.0;
double actual = service.calculateDistance(p1, p1); double actual = service.calculateDistance(p1, p1);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -61,8 +61,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Longitudinal-only movement") @DisplayName("Edge Case: Longitudinal-only movement")
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() { void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLatitude() {
var p1 = new LngLatDto(123.85, 983.2119); var p1 = new LngLat(123.85, 983.2119);
var p2 = new LngLatDto(133.85, 983.2119); var p2 = new LngLat(133.85, 983.2119);
double expected = 10.0; double expected = 10.0;
double actual = service.calculateDistance(p1, p2); double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION)); assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -71,8 +71,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Latitude-only movement") @DisplayName("Edge Case: Latitude-only movement")
void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() { void calculateDistance_shouldReturnCorrectDistance_forPointsWithSameLongitude() {
var p1 = new LngLatDto(123.85, 983.2119); var p1 = new LngLat(123.85, 983.2119);
var p2 = new LngLatDto(123.85, 973.2119); var p2 = new LngLat(123.85, 973.2119);
double expected = 10.0; double expected = 10.0;
double actual = service.calculateDistance(p1, p2); double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION)); assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -81,8 +81,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: Calculate with negative Coordinates") @DisplayName("General Case: Calculate with negative Coordinates")
void calculateDistance_shouldReturnCorrectEuclideanDistance_forNegativeCoordinates() { void calculateDistance_shouldReturnCorrectEuclideanDistance_forNegativeCoordinates() {
LngLatDto p1 = new LngLatDto(-1.0, -2.0); LngLat p1 = new LngLat(-1.0, -2.0);
LngLatDto p2 = new LngLatDto(2.0, 2.0); // lngDiff = 3, latDiff = 4 LngLat p2 = new LngLat(2.0, 2.0); // lngDiff = 3, latDiff = 4
double expected = 5.0; double expected = 5.0;
double actual = service.calculateDistance(p1, p2); double actual = service.calculateDistance(p1, p2);
assertThat(actual).isCloseTo(expected, within(PRECISION)); assertThat(actual).isCloseTo(expected, within(PRECISION));
@ -95,8 +95,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("False: Given Example For Testing") @DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() { void isCloseTo_shouldReturnFalse_givenExample() {
var p1 = new LngLatDto(-3.192473, 55.946233); var p1 = new LngLat(-3.192473, 55.946233);
var p2 = new LngLatDto(-3.192473, 55.942617); var p2 = new LngLat(-3.192473, 55.942617);
boolean expected = false; boolean expected = false;
boolean actual = service.isCloseTo(p1, p2); boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -105,7 +105,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("True: Two points are the same") @DisplayName("True: Two points are the same")
void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() { void isCloseTo_shouldReturnTrue_whenPointsAreIdentical() {
var p1 = new LngLatDto(151.86, 285.37); var p1 = new LngLat(151.86, 285.37);
boolean expected = true; boolean expected = true;
boolean actual = service.isCloseTo(p1, p1); boolean actual = service.isCloseTo(p1, p1);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -114,8 +114,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("True: Two points are close to each other and near threshold") @DisplayName("True: Two points are close to each other and near threshold")
void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() { void isCloseTo_shouldReturnTrue_whenCloseAndSmallerThanThreshold() {
var p1 = new LngLatDto(0.0, 0.0); var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLatDto(0.0, 0.00014); var p2 = new LngLat(0.0, 0.00014);
boolean expected = true; boolean expected = true;
boolean actual = service.isCloseTo(p1, p2); boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -124,8 +124,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("False: Distance nears the threshold") @DisplayName("False: Distance nears the threshold")
void isCloseTo_shouldReturnFalse_whenEqualsToThreshold() { void isCloseTo_shouldReturnFalse_whenEqualsToThreshold() {
var p1 = new LngLatDto(0.0, 0.0); var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLatDto(0.0, 0.00015); var p2 = new LngLat(0.0, 0.00015);
boolean expected = false; boolean expected = false;
boolean actual = service.isCloseTo(p1, p2); boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -134,8 +134,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("False: Distance larger to threshold") @DisplayName("False: Distance larger to threshold")
void isCloseTo_shouldReturnFalse_whenNotCloseAndLargerThanThreshold() { void isCloseTo_shouldReturnFalse_whenNotCloseAndLargerThanThreshold() {
var p1 = new LngLatDto(0.0, 0.0); var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLatDto(0.0, 0.00016); var p2 = new LngLat(0.0, 0.00016);
boolean expected = false; boolean expected = false;
boolean actual = service.isCloseTo(p1, p2); boolean actual = service.isCloseTo(p1, p2);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -149,10 +149,10 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: nextPosition in East direction (0 degrees)") @DisplayName("General Case: nextPosition in East direction (0 degrees)")
void nextPosition_shouldMoveEast_forAngleZero() { void nextPosition_shouldMoveEast_forAngleZero() {
var start = new LngLatDto(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 0; double angle = 0;
// For 0 degrees, cos(0)=1, sin(0)=0. Move happens entirely on lng axis. // 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); var actual = service.nextPosition(start, angle);
@ -163,10 +163,10 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)") @DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
void nextPosition_shouldMoveNorth_forAngle90() { void nextPosition_shouldMoveNorth_forAngle90() {
var start = new LngLatDto(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 90; double angle = 90;
// For 90 degrees, cos(90)=0, sin(90)=1. Move happens entirely on lat axis. // 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); var actual = service.nextPosition(start, angle);
@ -177,11 +177,11 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)") @DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
void nextPosition_shouldMoveWest_forAngle180() { void nextPosition_shouldMoveWest_forAngle180() {
var start = new LngLatDto(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 180; double angle = 180;
// For 180 degrees, cos(180)=-1, sin(180)=0. Move happens entirely on negative // For 180 degrees, cos(180)=-1, sin(180)=0. Move happens entirely on negative
// lng axis. // lng axis.
var expected = new LngLatDto(-STEP, 0.0); var expected = new LngLat(-STEP, 0.0);
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
@ -192,11 +192,11 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)") @DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
void nextPosition_shouldMoveSouth_forAngle270() { void nextPosition_shouldMoveSouth_forAngle270() {
var start = new LngLatDto(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 270; double angle = 270;
// For 270 degrees, cos(270)=0, sin(270)=-1. Move happens entirely on negative // For 270 degrees, cos(270)=0, sin(270)=-1. Move happens entirely on negative
// lat axis. // lat axis.
var expected = new LngLatDto(0.0, -STEP); var expected = new LngLat(0.0, -STEP);
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
@ -207,12 +207,12 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)") @DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
void nextPosition_shouldMoveNortheast_forAngle45() { void nextPosition_shouldMoveNortheast_forAngle45() {
var start = new LngLatDto(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 45; double angle = 45;
// Δlng = step * cos(45°), Δlat = step * sin(45°) // Δlng = step * cos(45°), Δlat = step * sin(45°)
double expectedLng = STEP * Math.cos(Math.toRadians(angle)); double expectedLng = STEP * Math.cos(Math.toRadians(angle));
double expectedLat = STEP * Math.sin(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); var actual = service.nextPosition(start, angle);
@ -223,13 +223,13 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Angle larger than 360 should wrap around") @DisplayName("Edge Case: Angle larger than 360 should wrap around")
void nextPosition_shouldHandleAngleGreaterThan360() { 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). // 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
double angle = 405; double angle = 405;
double equivalentAngle = 45; double equivalentAngle = 45;
double expectedLng = STEP * Math.cos(Math.toRadians(equivalentAngle)); double expectedLng = STEP * Math.cos(Math.toRadians(equivalentAngle));
double expectedLat = STEP * Math.sin(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); var actual = service.nextPosition(start, angle);
@ -240,12 +240,12 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Negative angle should work correctly") @DisplayName("Edge Case: Negative angle should work correctly")
void nextPosition_shouldHandleNegativeAngle() { 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. // A negative angle of -45° corresponds to the Southeast direction.
double angle = -45; double angle = -45;
double expectedLng = STEP * Math.cos(Math.toRadians(angle)); double expectedLng = STEP * Math.cos(Math.toRadians(angle));
double expectedLat = STEP * Math.sin(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); var actual = service.nextPosition(start, angle);
@ -258,17 +258,17 @@ public class GpsCalculationServiceTest {
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean") @DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
class CheckIsInRegionTests { class CheckIsInRegionTests {
public static final RegionDto RECTANGLE_REGION = new RegionDto("rectangle", List.of(new LngLatDto(0.0, 0.0), public static final Region RECTANGLE_REGION = new Region("rectangle", List.of(new LngLat(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))); new LngLat(2.0, 0.0), new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, 0.0)));
@Test @Test
@DisplayName("General Case: Given Example for Testing") @DisplayName("General Case: Given Example for Testing")
void isInRegion_shouldReturnFalse_givenPolygonCentral() { void isInRegion_shouldReturnFalse_givenPolygonCentral() {
var position = new LngLatDto(1.234, 1.222); var position = new LngLat(1.234, 1.222);
var region = new RegionDto("central", var region = new Region("central",
List.of(new LngLatDto(-3.192473, 55.946233), new LngLatDto(-3.192473, 55.942617), List.of(new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.942617),
new LngLatDto(-3.184319, 55.942617), new LngLatDto(-3.184319, 55.946233), new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.946233),
new LngLatDto(-3.192473, 55.946233))); new LngLat(-3.192473, 55.946233)));
boolean expected = false; boolean expected = false;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -277,7 +277,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: Simple Rectangle") @DisplayName("General Case: Simple Rectangle")
void isInRegion_shouldReturnTrue_forSimpleRectangle() { void isInRegion_shouldReturnTrue_forSimpleRectangle() {
var position = new LngLatDto(1.0, 1.0); var position = new LngLat(1.0, 1.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -286,7 +286,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: Simple Rectangle") @DisplayName("General Case: Simple Rectangle")
void isInRegion_shouldReturnFalse_forSimpleRectangle() { void isInRegion_shouldReturnFalse_forSimpleRectangle() {
var position = new LngLatDto(3.0, 1.0); var position = new LngLat(3.0, 1.0);
boolean expected = false; boolean expected = false;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -295,11 +295,11 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("General Case: Simple Hexagon") @DisplayName("General Case: Simple Hexagon")
void isInRegion_shouldReturnTrue_forSimpleHexagon() { void isInRegion_shouldReturnTrue_forSimpleHexagon() {
var position = new LngLatDto(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new RegionDto("hexagon", var region = new Region("hexagon",
List.of(new LngLatDto(1.0, 0.0), new LngLatDto(4.0, 0.0), new LngLatDto(5.0, 2.0), List.of(new LngLat(1.0, 0.0), new LngLat(4.0, 0.0), new LngLat(5.0, 2.0),
new LngLatDto(4.0, 4.0), new LngLatDto(1.0, 4.0), new LngLatDto(0.0, 2.0), new LngLat(4.0, 4.0), new LngLat(1.0, 4.0), new LngLat(0.0, 2.0),
new LngLatDto(1.0, 0.0))); new LngLat(1.0, 0.0)));
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -308,9 +308,9 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Small Triangle") @DisplayName("Edge Case: Small Triangle")
void isInRegion_shouldReturnTrue_forSmallTriangle() { void isInRegion_shouldReturnTrue_forSmallTriangle() {
var position = new LngLatDto(0.00001, 0.00001); var position = new LngLat(0.00001, 0.00001);
var region = new RegionDto("triangle", List.of(new LngLatDto(0.0, 0.0), new LngLatDto(0.0001, 0.0), var region = new Region("triangle", List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0),
new LngLatDto(0.00005, 0.0001), new LngLatDto(0.0, 0.0))); new LngLat(0.00005, 0.0001), new LngLat(0.0, 0.0)));
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -319,7 +319,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Point on Lower Edge of Rectangle") @DisplayName("Edge Case: Point on Lower Edge of Rectangle")
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() { void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
var position = new LngLatDto(0.0, 1.0); var position = new LngLat(0.0, 1.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -328,7 +328,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Point on Upper Edge of Rectangle") @DisplayName("Edge Case: Point on Upper Edge of Rectangle")
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() { void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
var position = new LngLatDto(2.0, 1.0); var position = new LngLat(2.0, 1.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -337,7 +337,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Point on Left Edge") @DisplayName("Edge Case: Point on Left Edge")
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() { void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
var position = new LngLatDto(0.0, 1.0); var position = new LngLat(0.0, 1.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -346,7 +346,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Point on Lower Vertex") @DisplayName("Edge Case: Point on Lower Vertex")
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() { void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
var position = new LngLatDto(0.0, 0.0); var position = new LngLat(0.0, 0.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -355,7 +355,7 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Point on Upper Vertex") @DisplayName("Edge Case: Point on Upper Vertex")
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() { void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
var position = new LngLatDto(2.0, 2.0); var position = new LngLat(2.0, 2.0);
boolean expected = true; boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION); boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -364,9 +364,9 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Region not forming polygon") @DisplayName("Edge Case: Region not forming polygon")
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() { void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
var position = new LngLatDto(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new RegionDto("line", var region = new Region("line",
List.of(new LngLatDto(0.0, 0.0), new LngLatDto(0.0001, 0.0), new LngLatDto(0.0, 0.0))); List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0), new LngLat(0.0, 0.0)));
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
service.checkIsInRegion(position, region); service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed."); }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
@ -375,9 +375,9 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Region is not closed") @DisplayName("Edge Case: Region is not closed")
void isInRegion_shouldThrowExceptions_whenRegionNotClose() { void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
var position = new LngLatDto(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new RegionDto("rectangle", List.of(new LngLatDto(0.0, 0.0), new LngLatDto(2.0, 0.0), var region = new Region("rectangle", List.of(new LngLat(0.0, 0.0), new LngLat(2.0, 0.0),
new LngLatDto(2.0, 2.0), new LngLatDto(0.0, 2.0), new LngLatDto(0.0, -1.0))); new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, -1.0)));
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
service.checkIsInRegion(position, region); service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed."); }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
@ -386,8 +386,8 @@ public class GpsCalculationServiceTest {
@Test @Test
@DisplayName("Edge Case: Vertex list is empty") @DisplayName("Edge Case: Vertex list is empty")
void isInRegion_shouldThrowExceptions_whenListIsEmpty() { void isInRegion_shouldThrowExceptions_whenListIsEmpty() {
var position = new LngLatDto(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new RegionDto("rectangle", List.of()); var region = new Region("rectangle", List.of());
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
service.checkIsInRegion(position, region); service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed."); }).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");