refractor(controller): Use List<> instead of []

This commit is contained in:
js0ny 2025-11-24 08:50:46 +00:00
parent 69d9e0d736
commit 141a957a8d
24 changed files with 991 additions and 380 deletions

3
.gitignore vendored
View file

@ -38,4 +38,5 @@ out/
*.tar *.tar
.direnv/ .direnv/
.envrc .envrc
localjson

View file

@ -1,4 +1,4 @@
FROM --platform=linux/amd64 openjdk:21 FROM maven:3.9.9-amazoncorretto-21-debian AS build
WORKDIR /app WORKDIR /app
@ -6,4 +6,4 @@ COPY ./build/libs/ilp-coursework-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View file

@ -20,6 +20,10 @@ body:json {
"capacity": 0.75, "capacity": 0.75,
"heating": true, "heating": true,
"maxCost": 13.5 "maxCost": 13.5
},
"delivery": {
"lng": -3.00,
"lat": 55.121
} }
}, },
{ {
@ -30,6 +34,10 @@ body:json {
"capacity": 0.75, "capacity": 0.75,
"heating": true, "heating": true,
"maxCost": 13.5 "maxCost": 13.5
},
"delivery": {
"lng": -3.00,
"lat": 55.121
} }
} }
] ]

View file

@ -21,6 +21,10 @@ body:json {
"cooling": false, "cooling": false,
"heating": true, "heating": true,
"maxCost": 13.5 "maxCost": 13.5
},
"delivery": {
"lng": -3.00,
"lat": 55.121
} }
} }
] ]

View file

@ -20,7 +20,8 @@ body:json {
"capacity": 0.75, "capacity": 0.75,
"heating": true, "heating": true,
"maxCost": 13.5 "maxCost": 13.5
} },
"delivery": null
} }
] ]
} }

View file

@ -1,18 +1,17 @@
package io.github.js0ny.ilp_coursework.controller; package io.github.js0ny.ilp_coursework.controller;
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 org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; 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.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;
/** /**
* Main REST Controller for the ILP Coursework 1 application. * Main REST Controller for the ILP Coursework 1 application.
* <p> * <p>
@ -53,7 +52,6 @@ public class ApiController {
*/ */
@PostMapping("/distanceTo") @PostMapping("/distanceTo")
public double getDistance(@RequestBody DistanceRequest request) { public double getDistance(@RequestBody DistanceRequest request) {
LngLat position1 = request.position1(); LngLat position1 = request.position1();
LngLat position2 = request.position2(); LngLat position2 = request.position2();
return gpsService.calculateDistance(position1, position2); return gpsService.calculateDistance(position1, position2);

View file

@ -1,11 +1,12 @@
package io.github.js0ny.ilp_coursework.controller; package io.github.js0ny.ilp_coursework.controller;
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.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.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
import io.github.js0ny.ilp_coursework.service.DroneInfoService; import io.github.js0ny.ilp_coursework.service.DroneInfoService;
import java.util.List;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@ -21,8 +22,8 @@ import org.springframework.web.client.RestTemplate;
@RequestMapping("/api/v1") @RequestMapping("/api/v1")
public class DroneController { public class DroneController {
private final DroneInfoService droneService; private final DroneInfoService droneInfoService;
private final RestTemplate restTemplate = new RestTemplate(); private final DroneAttrComparatorService droneAttrComparatorService;
/** /**
* Constructor of the {@code DroneController} with the business logic dependency * Constructor of the {@code DroneController} with the business logic dependency
@ -34,8 +35,12 @@ public class DroneController {
* *
* @param droneService The service component that contains all business logic * @param droneService The service component that contains all business logic
*/ */
public DroneController(DroneInfoService droneService) { public DroneController(
this.droneService = droneService; DroneInfoService droneService,
DroneAttrComparatorService droneAttrComparatorService
) {
this.droneInfoService = droneService;
this.droneAttrComparatorService = droneAttrComparatorService;
} }
/** /**
@ -47,8 +52,10 @@ public class DroneController {
* @return An array of drone id with cooling capability. * @return An array of drone id with cooling capability.
*/ */
@GetMapping("/dronesWithCooling/{state}") @GetMapping("/dronesWithCooling/{state}")
public String[] getDronesWithCoolingCapability(@PathVariable boolean state) { public List<String> getDronesWithCoolingCapability(
return droneService.dronesWithCooling(state); @PathVariable boolean state
) {
return droneInfoService.dronesWithCooling(state);
} }
/** /**
@ -61,7 +68,7 @@ public class DroneController {
@GetMapping("/droneDetails/{id}") @GetMapping("/droneDetails/{id}")
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) { public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
try { try {
Drone drone = droneService.droneDetail(id); Drone drone = droneInfoService.droneDetail(id);
return ResponseEntity.ok(drone); return ResponseEntity.ok(drone);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@ -77,30 +84,44 @@ public class DroneController {
* @return An array of drone id that matches the attribute name and value * @return An array of drone id that matches the attribute name and value
*/ */
@GetMapping("/queryAsPath/{attrName}/{attrVal}") @GetMapping("/queryAsPath/{attrName}/{attrVal}")
public String[] getIdByAttrMap( public List<String> getIdByAttrMap(
@PathVariable String attrName, @PathVariable String attrName,
@PathVariable String attrVal) { @PathVariable String attrVal
return droneService.dronesWithAttribute(attrName, attrVal); ) {
return droneAttrComparatorService.dronesWithAttribute(
attrName,
attrVal
);
} }
@PostMapping("/query") @PostMapping("/query")
public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) { public List<String> getIdByAttrMapPost(
return droneService.dronesSatisfyingAttributes(attrComparators); @RequestBody AttrQueryRequest[] attrComparators
) {
return droneAttrComparatorService.dronesSatisfyingAttributes(
attrComparators
);
} }
@PostMapping("/queryAvailableDrones") @PostMapping("/queryAvailableDrones")
public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) { public List<String> queryAvailableDrones(
return droneService.dronesMatchesRequirements(records); @RequestBody MedDispatchRecRequest[] records
) {
return droneInfoService.dronesMatchesRequirements(records);
} }
@PostMapping("/calcDeliveryPath") @PostMapping("/calcDeliveryPath")
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) { public DeliveryPathResponse calculateDeliveryPath(
return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{}); @RequestBody MedDispatchRecRequest[] record
) {
// return new DeliveryPathResponse(0.0f, 0, new DronePathDto[] {});
return null;
} }
@PostMapping("/calcDeliveryPathAsGeoJson") @PostMapping("/calcDeliveryPathAsGeoJson")
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) { public String calculateDeliveryPathAsGeoJson(
@RequestBody MedDispatchRecRequest[] record
) {
return "{}"; return "{}";
} }
} }

View file

@ -0,0 +1,14 @@
package io.github.js0ny.ilp_coursework.data.common;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
/**
* Represents a range of altitude values (that is a fly-zone in {@link RestrictedArea}).
*
* @param lower The lower bound of the altitude range.
* @param upper The upper bound of the altitude range. If {@code upper = -1}, then the region
* is not a fly zone.
*
*/
public record AltitudeRange(double lower, double upper) {
}

View file

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

View file

@ -8,4 +8,7 @@ package io.github.js0ny.ilp_coursework.data.common;
* @param lat latitude of the coordinate/point * @param lat latitude of the coordinate/point
*/ */
public record LngLat(double lng, double lat) { public record LngLat(double lng, double lat) {
public LngLat(LngLatAlt coord) {
this(coord.lng(), coord.lat());
}
} }

View file

@ -0,0 +1,12 @@
package io.github.js0ny.ilp_coursework.data.common;
/**
* Represents the data transfer object for a point or coordinate
* that defines by a longitude and latitude
*
* @param lng longitude of the coordinate/point
* @param lat latitude of the coordinate/point
* @param alt altitude of the coordinate/point
*/
public record LngLatAlt(double lng, double lat, double alt) {
}

View file

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

View file

@ -1,7 +1,8 @@
package io.github.js0ny.ilp_coursework.data.common; package io.github.js0ny.ilp_coursework.data.common;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest; import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -47,5 +48,4 @@ public record Region(String name, List<LngLat> vertices) {
LngLat last = vertices.getLast(); LngLat last = vertices.getLast();
return Objects.equals(last, first); return Objects.equals(last, first);
} }
} }

View file

@ -0,0 +1,23 @@
package io.github.js0ny.ilp_coursework.data.external;
import io.github.js0ny.ilp_coursework.data.common.AltitudeRange;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
import io.github.js0ny.ilp_coursework.data.common.Region;
import java.util.ArrayList;
import java.util.List;
public record RestrictedArea(
String name,
int id,
AltitudeRange limits,
LngLatAlt[] vertices
) {
public Region toRegion() {
List<LngLat> vertices2D = new ArrayList<>();
for (var vertex : vertices) {
vertices2D.add(new LngLat(vertex.lng(), vertex.lat()));
}
return new Region(name, vertices2D);
}
}

View file

@ -0,0 +1,5 @@
package io.github.js0ny.ilp_coursework.data.external;
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
public record ServicePoint(String name, int id, LngLatAlt location) {}

View file

@ -4,7 +4,7 @@ import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
public record ServicePointDrones( public record ServicePointDrones(
String servicePointId, int servicePointId,
DroneAvailability[] drones) { DroneAvailability[] drones) {
@Nullable @Nullable

View file

@ -1,15 +1,24 @@
package io.github.js0ny.ilp_coursework.data.request; package io.github.js0ny.ilp_coursework.data.request;
import io.github.js0ny.ilp_coursework.data.common.MedRequirement; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.github.js0ny.ilp_coursework.data.common.LngLat; import io.github.js0ny.ilp_coursework.data.common.LngLat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
@JsonIgnoreProperties(ignoreUnknown = true)
public record MedDispatchRecRequest( public record MedDispatchRecRequest(
int id, int id,
LocalDate date, LocalDate date,
LocalTime time, LocalTime time,
MedRequirement requirements, MedRequirement requirements,
LngLat delivery) { LngLat delivery) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record MedRequirement(
float capacity,
boolean cooling,
boolean heating,
float maxCost
) {
}
} }

View file

@ -1,9 +1,14 @@
package io.github.js0ny.ilp_coursework.data.response; package io.github.js0ny.ilp_coursework.data.response;
import io.github.js0ny.ilp_coursework.data.common.DronePathDto; import io.github.js0ny.ilp_coursework.data.common.LngLat;
import java.util.List;
public record DeliveryPathResponse( public record DeliveryPathResponse(
float totalCost, float totalCost,
int totalMoves, int totalMoves,
DronePathDto[] dronePaths) { DronePath[] dronePaths
) {
public record DronePath(int droneId, List<Delivery> deliveries) {
public record Delivery(int deliveryId, List<LngLat> flightPath) {}
}
} }

View file

@ -0,0 +1,140 @@
package io.github.js0ny.ilp_coursework.service;
import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.util.AttrOperator;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class DroneAttrComparatorService {
private final String baseUrl;
private final String dronesEndpoint = "drones";
private final RestTemplate restTemplate = new RestTemplate();
/**
* Constructor, handles the base url here.
*/
public DroneAttrComparatorService() {
String baseUrl = System.getenv("ILP_ENDPOINT");
if (baseUrl == null || baseUrl.isBlank()) {
this.baseUrl =
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
} else {
// Defensive: Add '/' to the end of the URL
if (!baseUrl.endsWith("/")) {
baseUrl += "/";
}
this.baseUrl = baseUrl;
}
}
/**
* Return an array of ids of drones with a given attribute name and value.
* <p>
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @return array of drone ids matching the attribute name and value
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
*/
public List<String> dronesWithAttribute(String attrName, String attrVal) {
// Call the helper with EQ operator
return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
}
/**
* Return an array of ids of drones which matches all given complex comparing
* rules
*
* @param attrComparators The filter rule with Name, Value and Operator
* @return array of drone ids that matches all rules
*/
public List<String> dronesSatisfyingAttributes(
AttrQueryRequest[] attrComparators
) {
Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) {
String attribute = comparator.attribute();
String operator = comparator.operator();
String value = comparator.value();
AttrOperator op = AttrOperator.fromString(operator);
List<String> ids = dronesWithAttributeCompared(
attribute,
value,
op
);
if (matchingDroneIds == null) {
matchingDroneIds = new HashSet<>(ids);
} else {
matchingDroneIds.retainAll(ids);
}
}
if (matchingDroneIds == null) {
return new ArrayList<>();
}
return matchingDroneIds.stream().toList();
}
/**
* Helper that wraps the dynamic querying with different comparison operators
* <p>
* This method act as a concatenation of
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
* AttrOperator)}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @param op the comparison operator
* @return array of drone ids matching the attribute name and value (filtered by
* {@code op})
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
* String,
* AttrOperator)
*/
private List<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
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
if (drones == null) {
return new ArrayList<>();
}
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
// querying
ObjectMapper mapper = new ObjectMapper();
return Arrays.stream(drones)
.filter(drone -> {
JsonNode node = mapper.valueToTree(drone);
JsonNode attrNode = node.findValue(attrName);
if (attrNode != null) {
// Manually handle different types of JsonNode
return isValueMatched(attrNode, attrVal, op);
} else {
return false;
}
})
.map(Drone::id)
.collect(Collectors.toList());
}
}

View file

@ -1,33 +1,38 @@
package io.github.js0ny.ilp_coursework.service; package io.github.js0ny.ilp_coursework.service;
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; import static io.github.js0ny.ilp_coursework.util.AttrComparator.isValueMatched;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.external.Drone;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery;
import io.github.js0ny.ilp_coursework.util.AttrOperator;
import java.net.URI; import java.net.URI;
import java.time.DayOfWeek; import java.time.DayOfWeek;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
import java.util.Arrays; import java.util.*;
import java.util.HashSet; import java.util.stream.Collectors;
import java.util.Set; import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service @Service
public class DroneInfoService { public class DroneInfoService {
private final String baseUrl; private final String baseUrl;
private final String dronesEndpoint = "drones"; private final String dronesEndpoint = "drones";
private final String dronesForServicePointsEndpoint = "drones-for-service-points"; private final String dronesForServicePointsEndpoint =
"drones-for-service-points";
public static final String servicePointsEndpoint = "service-points";
public static final String restrictedAreasEndpoint = "restricted-areas";
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
@ -37,7 +42,8 @@ public class DroneInfoService {
public DroneInfoService() { public DroneInfoService() {
String baseUrl = System.getenv("ILP_ENDPOINT"); String baseUrl = System.getenv("ILP_ENDPOINT");
if (baseUrl == null || baseUrl.isBlank()) { if (baseUrl == null || baseUrl.isBlank()) {
this.baseUrl = "https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/"; this.baseUrl =
"https://ilp-rest-2025-bvh6e9hschfagrgy.ukwest-01.azurewebsites.net/";
} else { } else {
// Defensive: Add '/' to the end of the URL // Defensive: Add '/' to the end of the URL
if (!baseUrl.endsWith("/")) { if (!baseUrl.endsWith("/")) {
@ -57,20 +63,20 @@ public class DroneInfoService {
* capability, else without cooling * capability, else without cooling
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
*/ */
public String[] dronesWithCooling(boolean state) { public List<String> dronesWithCooling(boolean state) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); // URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
Drone[] drones = restTemplate.getForObject( // Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
droneUrl, List<Drone> drones = fetchAllDrones();
Drone[].class);
if (drones == null) { if (drones == null) {
return new String[]{}; return new ArrayList<>();
} }
return Arrays.stream(drones) return drones
.filter(drone -> drone.capability().cooling() == state) .stream()
.map(Drone::id) .filter(drone -> drone.capability().cooling() == state)
.toArray(String[]::new); .map(Drone::id)
.collect(Collectors.toList());
} }
/** /**
@ -88,10 +94,7 @@ public class DroneInfoService {
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String) * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
*/ */
public Drone droneDetail(String id) { public Drone droneDetail(String id) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); List<Drone> drones = fetchAllDrones();
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
if (drones == null) { if (drones == null) {
throw new NullPointerException("drone cannot be found"); throw new NullPointerException("drone cannot be found");
@ -105,96 +108,8 @@ public class DroneInfoService {
// This will result in 404 // This will result in 404
throw new IllegalArgumentException( throw new IllegalArgumentException(
"drone with that ID cannot be found"); "drone with that ID cannot be found"
} );
/**
* Return an array of ids of drones with a given attribute name and value.
* <p>
* Associated service method with {@code /queryAsPath/{attrName}/{attrVal}}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @return array of drone ids matching the attribute name and value
* @see #dronesWithAttributeCompared(String, String, AttrOperator)
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getIdByAttrMap
*/
public String[] dronesWithAttribute(String attrName, String attrVal) {
// Call the helper with EQ operator
return dronesWithAttributeCompared(attrName, attrVal, AttrOperator.EQ);
}
/**
* Return an array of ids of drones which matches all given complex comparing
* rules
*
* @param attrComparators The filter rule with Name, Value and Operator
* @return array of drone ids that matches all rules
*/
public String[] dronesSatisfyingAttributes(AttrQueryRequest[] attrComparators) {
Set<String> matchingDroneIds = null;
for (var comparator : attrComparators) {
String attribute = comparator.attribute();
String operator = comparator.operator();
String value = comparator.value();
AttrOperator op = AttrOperator.fromString(operator);
String[] ids = dronesWithAttributeCompared(attribute, value, op);
if (matchingDroneIds == null) {
matchingDroneIds = new HashSet<>(Arrays.asList(ids));
} else {
matchingDroneIds.retainAll(Arrays.asList(ids));
}
}
if (matchingDroneIds == null) {
return new String[]{};
}
return matchingDroneIds.toArray(String[]::new);
}
/**
* Helper that wraps the dynamic querying with different comparison operators
* <p>
* This method act as a concatenation of
* {@link io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode, String,
* AttrOperator)}
*
* @param attrName the attribute name to filter on
* @param attrVal the attribute value to filter on
* @param op the comparison operator
* @return array of drone ids matching the attribute name and value (filtered by
* {@code op})
* @see io.github.js0ny.ilp_coursework.util.AttrComparator#isValueMatched(JsonNode,
* String,
* AttrOperator)
*/
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
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
if (drones == null) {
return new String[]{};
}
// Use Jackson's ObjectMapper to convert DroneDto to JsonNode for dynamic
// querying
ObjectMapper mapper = new ObjectMapper();
return Arrays.stream(drones)
.filter(drone -> {
JsonNode node = mapper.valueToTree(drone);
JsonNode attrNode = node.findValue(attrName);
if (attrNode != null) {
// Manually handle different types of JsonNode
return isValueMatched(attrNode, attrVal, op);
} else {
return false;
}
})
.map(Drone::id)
.toArray(String[]::new);
} }
/** /**
@ -207,27 +122,34 @@ 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(MedDispatchRecRequest[] rec) { public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint); List<Drone> drones = fetchAllDrones();
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
if (drones == null || rec == null || rec.length == 0) { if (drones == null) {
return new String[]{}; return new ArrayList<>();
}
if (rec == null || rec.length == 0) {
return drones
.stream()
.filter(Objects::nonNull)
.map(Drone::id)
.collect(Collectors.toList());
} }
/* /*
* Traverse and filter drones, pass every record's requirement to helper * Traverse and filter drones, pass every record's requirement to helper
*/ */
return Arrays.stream(drones) return drones
.filter(drone -> drone != null && drone.capability() != null) .stream()
.filter(drone -> Arrays.stream(rec) .filter(d -> d != null && d.capability() != null)
.filter(record -> record != null && record.requirements() != null) .filter(d ->
// Every record must be met Arrays.stream(rec)
.allMatch(record -> meetsRequirement(drone, record))) .filter(r -> r != null && r.requirements() != null)
.map(Drone::id) .allMatch(r -> meetsRequirement(d, r))
.toArray(String[]::new); )
.map(Drone::id)
.collect(Collectors.toList());
} }
/** /**
@ -240,14 +162,16 @@ 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(Drone drone, MedDispatchRecRequest record) { public 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");
} }
var capability = drone.capability(); var capability = drone.capability();
if (capability == null) { if (capability == null) {
throw new IllegalArgumentException("drone capability cannot be null"); throw new IllegalArgumentException(
"drone capability cannot be null"
);
} }
float requiredCapacity = requirements.capacity(); float requiredCapacity = requirements.capacity();
@ -256,24 +180,24 @@ public class DroneInfoService {
} }
// Use boolean wrapper to allow null (not specified) values // Use boolean wrapper to allow null (not specified) values
Boolean requiredCooling = requirements.cooling(); boolean requiredCooling = requirements.cooling();
Boolean requiredHeating = requirements.heating(); boolean requiredHeating = requirements.heating();
Float requiredMaxCost = requirements.maxCost();
boolean matchesCooling = requiredCooling == null || capability.cooling() == requiredCooling; // Case 1: required is null: We don't care about it
boolean matchesHeating = requiredHeating == null || capability.heating() == requiredHeating; // Case 2: required is false: We don't care about it (high capability adapts to low requirements)
boolean matchesCost = false; // Case 3: capability is true: Then always matches
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
float totalCost = capability.costInitial() + capability.costFinal(); boolean matchesCooling = !requiredCooling || capability.cooling();
boolean matchesHeating = !requiredHeating || capability.heating();
if (capability.maxMoves() > 0) {
totalCost += capability.costPerMove();
}
matchesCost = totalCost <= requiredMaxCost;
// Conditions: All requirements matched + availability matched, use helper // Conditions: All requirements matched + availability matched, use helper
// For minimal privilege, only pass drone id to check availability // For minimal privilege, only pass drone id to check availability
return matchesCooling && matchesHeating && matchesCost && checkAvailability(drone.id(), record); return (
matchesCooling &&
matchesHeating &&
checkAvailability(drone.id(), record)
); // &&
// checkCost(drone, record) // checkCost is more expensive than checkAvailability
} }
/** /**
@ -284,25 +208,213 @@ 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, MedDispatchRecRequest record) { private boolean checkAvailability(
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); String droneId,
MedDispatchRecRequest record
) {
URI droneUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
ServicePointDrones[] servicePoints = restTemplate.getForObject( ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl, droneUrl,
ServicePointDrones[].class); ServicePointDrones[].class
);
LocalDate requiredDate = record.date(); LocalDate requiredDate = record.date();
DayOfWeek requiredDay = requiredDate.getDayOfWeek(); DayOfWeek requiredDay = requiredDate.getDayOfWeek();
LocalTime requiredTime = record.time(); LocalTime requiredTime = record.time();
assert servicePoints != null;
for (var servicePoint : servicePoints) { for (var servicePoint : servicePoints) {
var drone = servicePoint.locateDroneById(droneId); // Nullable var drone = servicePoint.locateDroneById(droneId); // Nullable
if (drone != null) { if (drone != null) {
return drone.checkAvailability(requiredDay, requiredTime); return drone.checkAvailability(requiredDay, requiredTime);
} }
} }
return false; return false;
}
private LngLat queryServicePointLocationByDroneId(String droneId) {
URI droneUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl,
ServicePointDrones[].class
);
assert servicePoints != null;
for (var sp : servicePoints) {
var drone = sp.locateDroneById(droneId); // Nullable
if (drone != null) {
return queryServicePointLocation(sp.servicePointId());
}
}
return null;
}
@Nullable
private LngLat queryServicePointLocation(int id) {
URI servicePointUrl = URI.create(baseUrl).resolve(
servicePointsEndpoint
);
ServicePoint[] servicePoints = restTemplate.getForObject(
servicePointUrl,
ServicePoint[].class
);
assert servicePoints != null;
for (var sp : servicePoints) {
if (sp.id() == id) {
// We dont consider altitude
return new LngLat(sp.location());
}
}
return null;
}
// private Set<LngLat> parseObstacles() {
// URI restrictedAreasUrl = URI.create(baseUrl).resolve(
// restrictedAreasEndpoint
// );
//
// RestrictedArea[] restrictedAreas = restTemplate.getForObject(
// restrictedAreasUrl,
// RestrictedArea[].class
// );
//
// assert restrictedAreas != null;
// Set<LngLat> obstacles = new HashSet<>();
// for (var ra : restrictedAreas) {
// obstacles.add(new LngLat(ra.location()));
// }
// return obstacles;
// }
// public DeliveryPathResponse calcDeliveryPath(
// MedDispatchRecRequest[] records
// ) {
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
// Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
// List<RestrictedArea> restrictedAreas = fetchRestrictedAreas();
// List<LngLat> totalPath = new ArrayList<>();
// List<Delivery> deliveries = new ArrayList<>();
// int moves = 0;
// float cost = 0;
// for (var record : records) {
// assert drones != null;
// Drone[] possibleDrones = Arrays.stream(drones)
// .filter(d -> meetsRequirement(d, record))
// .toArray(Drone[]::new);
// int shortestPathCount = Integer.MAX_VALUE;
// float lowestCost = Float.MAX_VALUE;
// List<LngLat> shortestPath = null;
// for (var d : possibleDrones) {
// var start = queryServicePointLocationByDroneId(d.id());
// List<LngLat> path = PathFinderService.findPath(
// start,
// record.delivery(),
// restrictedAreas
// );
// float pathCost = path.size() * d.capability().costPerMove();
// if (
// path.size() < d.capability().maxMoves() &&
// pathCost < lowestCost
// ) {
// shortestPathCount = path.size();
// lowestCost = pathCost;
// shortestPath = path;
// }
// }
// // deliveries.add(new Delivery(record.id(), shortestPath));
// }
// // return new
// }
private List<Drone> fetchAllDrones() {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
return Arrays.asList(drones);
}
private List<RestrictedArea> fetchRestrictedAreas() {
URI restrictedUrl = URI.create(baseUrl).resolve(
restrictedAreasEndpoint
);
RestrictedArea[] restrictedAreas = restTemplate.getForObject(
restrictedUrl,
RestrictedArea[].class
);
assert restrictedAreas != null;
List<RestrictedArea> restrictedAreaList = Arrays.asList(
restrictedAreas
);
return restrictedAreaList;
}
private List<ServicePoint> fetchServicePoints() {
URI servicePointUrl = URI.create(baseUrl).resolve(
servicePointsEndpoint
);
ServicePoint[] servicePoints = restTemplate.getForObject(
servicePointUrl,
ServicePoint[].class
);
assert servicePoints != null;
List<ServicePoint> servicePointList = Arrays.asList(servicePoints);
return servicePointList;
}
private List<ServicePointDrones> fetchDronesForServicePoints() {
URI servicePointDronesUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
ServicePointDrones[] servicePointDrones = restTemplate.getForObject(
servicePointDronesUrl,
ServicePointDrones[].class
);
assert servicePointDrones != null;
List<ServicePointDrones> servicePointDronesList = Arrays.asList(
servicePointDrones
);
return servicePointDronesList;
}
// NOTE: Not used.
private boolean checkCost(Drone drone, MedDispatchRecRequest rec) {
if (rec.delivery() == null) {
return true;
}
URI droneUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl,
ServicePointDrones[].class
);
GpsCalculationService gpsService = new GpsCalculationService();
double steps = gpsService.calculateSteps(
queryServicePointLocationByDroneId(drone.id()),
rec.delivery()
);
if (steps > drone.capability().maxMoves()) {
return false;
}
float baseCost =
drone.capability().costInitial() + drone.capability().costFinal();
double cost = baseCost + drone.capability().costPerMove() * steps;
var requiredMaxCost = rec.requirements().maxCost();
return cost <= requiredMaxCost;
} }
} }

View file

@ -1,12 +1,11 @@
package io.github.js0ny.ilp_coursework.service; package io.github.js0ny.ilp_coursework.service;
import java.util.List;
import io.github.js0ny.ilp_coursework.data.common.LngLat; 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.common.Region;
import io.github.js0ny.ilp_coursework.data.request.DistanceRequest; 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.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest; import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@ -49,6 +48,11 @@ public class GpsCalculationService {
return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance); return Math.sqrt(lngDistance * lngDistance + latDistance * latDistance);
} }
public double calculateSteps(LngLat position1, LngLat position2) {
double distance = calculateDistance(position1, position2);
return distance / STEP;
}
/** /**
* 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
@ -99,8 +103,10 @@ public class GpsCalculationService {
* @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest) * @see io.github.js0ny.ilp_coursework.controller.ApiController#getIsInRegion(RegionCheckRequest)
* @see Region#isClosed() * @see Region#isClosed()
*/ */
public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException { public boolean checkIsInRegion(LngLat position, Region region)
if (!region.isClosed()) { // call method from RegionDto to check if not closed throws IllegalArgumentException {
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.");
} }
return rayCasting(position, region.vertices()); return rayCasting(position, region.vertices());
@ -146,7 +152,10 @@ public class GpsCalculationService {
continue; continue;
} }
double xIntersection = a.lng() + ((point.lat() - a.lat()) * (b.lng() - a.lng())) / (b.lat() - a.lat()); double xIntersection =
a.lng() +
((point.lat() - a.lat()) * (b.lng() - a.lng())) /
(b.lat() - a.lat());
if (xIntersection > point.lng()) { if (xIntersection > point.lng()) {
++intersections; ++intersections;
@ -171,14 +180,19 @@ public class GpsCalculationService {
*/ */
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat 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.lat() - a.lat()) * (b.lng() - a.lng()); (p.lng() - a.lng()) * (b.lat() - a.lat()) -
(p.lat() - a.lat()) * (b.lng() - a.lng());
if (Math.abs(crossProduct) > 1e-9) { if (Math.abs(crossProduct) > 1e-9) {
return false; return false;
} }
boolean isWithinLng = p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng()); boolean isWithinLng =
boolean isWithinLat = p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat()); p.lng() >= Math.min(a.lng(), b.lng()) &&
p.lng() <= Math.max(a.lng(), b.lng());
boolean isWithinLat =
p.lat() >= Math.min(a.lat(), b.lat()) &&
p.lat() <= Math.max(a.lat(), b.lat());
return isWithinLng && isWithinLat; return isWithinLng && isWithinLat;
} }

View file

@ -0,0 +1,143 @@
package io.github.js0ny.ilp_coursework.service;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
@Service
public class PathFinderService {
private final GpsCalculationService service;
public PathFinderService(GpsCalculationService gpsCalculationService) {
this.service = gpsCalculationService;
}
private static class Node implements Comparable<Node> {
final LngLat point;
Node parent;
double g;
double h;
double f;
public Node(LngLat point, Node parent, double g, double h) {
this.point = point;
this.parent = parent;
this.g = g;
this.h = h;
this.f = g + h;
}
@Override
public int compareTo(Node other) {
return Double.compare(this.f, other.f);
}
}
public static List<LngLat> findPath(
LngLat start,
LngLat target,
List<RestrictedArea> restricted
) {
var service = new GpsCalculationService();
PriorityQueue<Node> openSet = new PriorityQueue<>();
Map<LngLat, Double> allNodesMinG = new HashMap<>();
if (checkIsInRestrictedAreas(target, restricted)) {
return Collections.emptyList();
}
Node startNode = new Node(
start,
null,
0,
service.calculateDistance(start, target)
);
openSet.add(startNode);
allNodesMinG.put(start, 0.0);
while (!openSet.isEmpty()) {
Node current = openSet.poll();
if (service.isCloseTo(current.point, target)) {
return reconstructPath(current);
}
if (
current.g >
allNodesMinG.getOrDefault(current.point, Double.MAX_VALUE)
) {
continue;
}
for (LngLat neighbour : getNeighbours(current.point)) {
if (checkIsInRestrictedAreas(neighbour, restricted)) {
continue;
}
double newG = current.g + 0.00015;
if (newG < allNodesMinG.getOrDefault(neighbour, Double.MAX_VALUE)) {
double newH = service.calculateDistance(neighbour, target);
Node neighbourNode = new Node(neighbour, current, newG, newH);
allNodesMinG.put(neighbour, newG);
openSet.add(neighbourNode);
}
}
}
return Collections.emptyList();
}
private static List<LngLat> reconstructPath(Node endNode) {
LinkedList<LngLat> path = new LinkedList<>();
Node curr = endNode;
while (curr != null) {
path.addFirst(curr.point);
curr = curr.parent;
}
return path;
}
private static boolean checkIsInRestrictedAreas(
LngLat point,
List<RestrictedArea> RestrictedAreas
) {
var service = new GpsCalculationService();
for (RestrictedArea area : RestrictedAreas) {
Region r = area.toRegion();
if (service.checkIsInRegion(point, r)) {
return true;
}
}
return false;
}
private static List<LngLat> getNeighbours(LngLat p) {
var service = new GpsCalculationService();
double angle = 0;
List<LngLat> positions = new ArrayList<>();
final int directionCount = 8;
for (int i = 0; i < directionCount; i++) {
double directionAngle = angle + (i * 45);
LngLat nextPosition = service.nextPosition(p, directionAngle);
positions.add(nextPosition);
}
return positions;
}
}

View file

@ -14,9 +14,7 @@ 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.MovementRequest;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest; 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;
@ -61,22 +59,19 @@ 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";
LngLat p1 = new LngLat(0, 4.0); LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0); LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2); var req = new DistanceRequest(p1, p2);
when( when(
service.calculateDistance( service.calculateDistance(any(LngLat.class), any(LngLat.class))
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());
@ -88,23 +83,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(LngLat.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());
} }
} }
@ -115,19 +110,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";
LngLat p1 = new LngLat(0, 4.0); LngLat p1 = new LngLat(0, 4.0);
LngLat p2 = new LngLat(3.0, 0); LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2); var req = new DistanceRequest(p1, p2);
when( when(
service.isCloseTo(any(LngLat.class), any(LngLat.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());
@ -137,19 +132,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());
} }
} }
@ -162,46 +157,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 {
LngLat expected = new LngLat(0.00015, 0.0); LngLat expected = new LngLat(0.00015, 0.0);
LngLat p = new LngLat(0, 0); LngLat p = new LngLat(0, 0);
var req = new MovementRequest(p, 0); var req = new MovementRequest(p, 0);
when( when(
service.nextPosition(any(LngLat.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());
} }
} }
@ -212,31 +207,28 @@ 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 LngLat(1.234, 1.222); var position = new LngLat(1.234, 1.222);
var region = new Region( var region = new Region(
"central", "central",
List.of( List.of(
new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.946233),
new LngLat(-3.192473, 55.942617), new LngLat(-3.192473, 55.942617),
new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.942617),
new LngLat(-3.184319, 55.946233), new LngLat(-3.184319, 55.946233),
new LngLat(-3.192473, 55.946233) new LngLat(-3.192473, 55.946233)
) )
); );
var req = new RegionCheckRequest(position, region); var req = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(any(LngLat.class), any(Region.class))
any(LngLat.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());
@ -245,59 +237,53 @@ 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 LngLat(1, 1); var position = new LngLat(1, 1);
var region = new Region("illegal", List.of()); var region = new Region("illegal", List.of());
var request = new RegionCheckRequest(position, region); var request = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(any(LngLat.class), any(Region.class))
any(LngLat.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 LngLat(1, 1); var position = new LngLat(1, 1);
var region = new Region( var region = new Region(
"illegal", "illegal",
List.of( List.of(
new LngLat(1, 2), new LngLat(1, 2),
new LngLat(3, 4), new LngLat(3, 4),
new LngLat(5, 6), new LngLat(5, 6),
new LngLat(7, 8), new LngLat(7, 8),
new LngLat(9, 10) new LngLat(9, 10)
) )
); );
var request = new RegionCheckRequest(position, region); var request = new RegionCheckRequest(position, region);
when( when(
service.checkIsInRegion( service.checkIsInRegion(any(LngLat.class), any(Region.class))
any(LngLat.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,18 +1,17 @@
package io.github.js0ny.ilp_coursework.service; package io.github.js0ny.ilp_coursework.service;
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;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.within; import static org.assertj.core.api.AssertionsForClassTypes.within;
import io.github.js0ny.ilp_coursework.data.common.LngLat;
import io.github.js0ny.ilp_coursework.data.common.Region;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class GpsCalculationServiceTest { public class GpsCalculationServiceTest {
private static final double STEP = 0.00015; private static final double STEP = 0.00015;
@ -29,6 +28,7 @@ public class GpsCalculationServiceTest {
@Nested @Nested
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double") @DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
class CalculateDistanceTests { class CalculateDistanceTests {
@Test @Test
@DisplayName("False: Given Example For Testing") @DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() { void isCloseTo_shouldReturnFalse_givenExample() {
@ -92,6 +92,7 @@ public class GpsCalculationServiceTest {
@Nested @Nested
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean") @DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
class IsCloseToTests { class IsCloseToTests {
@Test @Test
@DisplayName("False: Given Example For Testing") @DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() { void isCloseTo_shouldReturnFalse_givenExample() {
@ -112,7 +113,9 @@ 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 LngLat(0.0, 0.0); var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00014); var p2 = new LngLat(0.0, 0.00014);
@ -156,12 +159,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@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 LngLat(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 90; double angle = 90;
@ -170,12 +181,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@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 LngLat(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 180; double angle = 180;
@ -185,12 +204,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@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 LngLat(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 270; double angle = 270;
@ -200,12 +227,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@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 LngLat(0.0, 0.0); var start = new LngLat(0.0, 0.0);
double angle = 45; double angle = 45;
@ -216,8 +251,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@Test @Test
@ -227,14 +268,22 @@ public class GpsCalculationServiceTest {
// 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 =
double expectedLat = STEP * Math.sin(Math.toRadians(equivalentAngle)); STEP * Math.cos(Math.toRadians(equivalentAngle));
double expectedLat =
STEP * Math.sin(Math.toRadians(equivalentAngle));
var expected = new LngLat(expectedLng, expectedLat); var expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
@Test @Test
@ -249,8 +298,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle); var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION)); assertThat(actual.lng()).isCloseTo(
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION)); expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
} }
} }
@ -258,17 +313,31 @@ public class GpsCalculationServiceTest {
@DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean") @DisplayName("Test for checkIsInRegion(LngLatDto, RegionDto) -> boolean")
class CheckIsInRegionTests { class CheckIsInRegionTests {
public static final Region RECTANGLE_REGION = new Region("rectangle", List.of(new LngLat(0.0, 0.0), public static final Region RECTANGLE_REGION = new Region(
new LngLat(2.0, 0.0), new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, 0.0))); "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 @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 LngLat(1.234, 1.222); var position = new LngLat(1.234, 1.222);
var region = new Region("central", var region = new Region(
List.of(new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.942617), "central",
new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.946233), List.of(
new LngLat(-3.192473, 55.946233))); 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 expected = false;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -279,7 +348,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_forSimpleRectangle() { void isInRegion_shouldReturnTrue_forSimpleRectangle() {
var position = new LngLat(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);
} }
@ -288,7 +360,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnFalse_forSimpleRectangle() { void isInRegion_shouldReturnFalse_forSimpleRectangle() {
var position = new LngLat(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);
} }
@ -296,10 +371,18 @@ public class GpsCalculationServiceTest {
@DisplayName("General Case: Simple Hexagon") @DisplayName("General Case: Simple Hexagon")
void isInRegion_shouldReturnTrue_forSimpleHexagon() { void isInRegion_shouldReturnTrue_forSimpleHexagon() {
var position = new LngLat(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new Region("hexagon", var region = new Region(
List.of(new LngLat(1.0, 0.0), new LngLat(4.0, 0.0), new LngLat(5.0, 2.0), "hexagon",
new LngLat(4.0, 4.0), new LngLat(1.0, 4.0), new LngLat(0.0, 2.0), List.of(
new LngLat(1.0, 0.0))); 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 expected = true;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -309,8 +392,15 @@ public class GpsCalculationServiceTest {
@DisplayName("Edge Case: Small Triangle") @DisplayName("Edge Case: Small Triangle")
void isInRegion_shouldReturnTrue_forSmallTriangle() { void isInRegion_shouldReturnTrue_forSmallTriangle() {
var position = new LngLat(0.00001, 0.00001); 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), var region = new Region(
new LngLat(0.00005, 0.0001), new LngLat(0.0, 0.0))); "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 expected = true;
boolean actual = service.checkIsInRegion(position, region); boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
@ -321,7 +411,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() { void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
var position = new LngLat(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);
} }
@ -330,7 +423,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() { void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
var position = new LngLat(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);
} }
@ -339,7 +435,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() { void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
var position = new LngLat(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);
} }
@ -348,7 +447,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() { void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
var position = new LngLat(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);
} }
@ -357,7 +459,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() { void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
var position = new LngLat(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);
} }
@ -365,22 +470,40 @@ public class GpsCalculationServiceTest {
@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 LngLat(2.0, 2.0); var position = new LngLat(2.0, 2.0);
var region = new Region("line", var region = new Region(
List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0), new LngLat(0.0, 0.0))); "line",
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.");
} }
@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 LngLat(2.0, 2.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), var region = new Region(
new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, -1.0))); "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(() -> { 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.");
} }
@Test @Test
@ -390,7 +513,9 @@ public class GpsCalculationServiceTest {
var region = new Region("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.");
} }
} }
} }