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

1
.gitignore vendored
View file

@ -39,3 +39,4 @@ out/
*.tar
.direnv/
.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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,17 @@
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.github.js0ny.ilp_coursework.data.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.
* <p>
@ -53,7 +52,6 @@ public class ApiController {
*/
@PostMapping("/distanceTo")
public double getDistance(@RequestBody DistanceRequest request) {
LngLat position1 = request.position1();
LngLat position2 = request.position2();
return gpsService.calculateDistance(position1, position2);

View file

@ -1,11 +1,12 @@
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.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.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 java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@ -21,8 +22,8 @@ import org.springframework.web.client.RestTemplate;
@RequestMapping("/api/v1")
public class DroneController {
private final DroneInfoService droneService;
private final RestTemplate restTemplate = new RestTemplate();
private final DroneInfoService droneInfoService;
private final DroneAttrComparatorService droneAttrComparatorService;
/**
* 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
*/
public DroneController(DroneInfoService droneService) {
this.droneService = droneService;
public DroneController(
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.
*/
@GetMapping("/dronesWithCooling/{state}")
public String[] getDronesWithCoolingCapability(@PathVariable boolean state) {
return droneService.dronesWithCooling(state);
public List<String> getDronesWithCoolingCapability(
@PathVariable boolean state
) {
return droneInfoService.dronesWithCooling(state);
}
/**
@ -61,7 +68,7 @@ public class DroneController {
@GetMapping("/droneDetails/{id}")
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
try {
Drone drone = droneService.droneDetail(id);
Drone drone = droneInfoService.droneDetail(id);
return ResponseEntity.ok(drone);
} catch (IllegalArgumentException ex) {
return ResponseEntity.notFound().build();
@ -77,30 +84,44 @@ public class DroneController {
* @return An array of drone id that matches the attribute name and value
*/
@GetMapping("/queryAsPath/{attrName}/{attrVal}")
public String[] getIdByAttrMap(
public List<String> getIdByAttrMap(
@PathVariable String attrName,
@PathVariable String attrVal) {
return droneService.dronesWithAttribute(attrName, attrVal);
@PathVariable String attrVal
) {
return droneAttrComparatorService.dronesWithAttribute(
attrName,
attrVal
);
}
@PostMapping("/query")
public String[] getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
return droneService.dronesSatisfyingAttributes(attrComparators);
public List<String> getIdByAttrMapPost(
@RequestBody AttrQueryRequest[] attrComparators
) {
return droneAttrComparatorService.dronesSatisfyingAttributes(
attrComparators
);
}
@PostMapping("/queryAvailableDrones")
public String[] queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
return droneService.dronesMatchesRequirements(records);
public List<String> queryAvailableDrones(
@RequestBody MedDispatchRecRequest[] records
) {
return droneInfoService.dronesMatchesRequirements(records);
}
@PostMapping("/calcDeliveryPath")
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
return new DeliveryPathResponse(0.0f, 0, new DronePathDto[]{});
public DeliveryPathResponse calculateDeliveryPath(
@RequestBody MedDispatchRecRequest[] record
) {
// return new DeliveryPathResponse(0.0f, 0, new DronePathDto[] {});
return null;
}
@PostMapping("/calcDeliveryPathAsGeoJson")
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
public String calculateDeliveryPathAsGeoJson(
@RequestBody MedDispatchRecRequest[] record
) {
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
*/
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;
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@ -47,5 +48,4 @@ public record Region(String name, List<LngLat> vertices) {
LngLat last = vertices.getLast();
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;
public record ServicePointDrones(
String servicePointId,
int servicePointId,
DroneAvailability[] drones) {
@Nullable

View file

@ -1,15 +1,24 @@
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 java.time.LocalDate;
import java.time.LocalTime;
@JsonIgnoreProperties(ignoreUnknown = true)
public record MedDispatchRecRequest(
int id,
LocalDate date,
LocalTime time,
MedRequirement requirements,
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;
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(
float totalCost,
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;
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 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.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class DroneInfoService {
private final String baseUrl;
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();
@ -37,7 +42,8 @@ public class DroneInfoService {
public DroneInfoService() {
String baseUrl = System.getenv("ILP_ENDPOINT");
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 {
// Defensive: Add '/' to the end of the URL
if (!baseUrl.endsWith("/")) {
@ -57,20 +63,20 @@ public class DroneInfoService {
* capability, else without cooling
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
*/
public String[] dronesWithCooling(boolean state) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
public List<String> dronesWithCooling(boolean state) {
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
// Drone[] drones = restTemplate.getForObject(droneUrl, Drone[].class);
List<Drone> drones = fetchAllDrones();
if (drones == null) {
return new String[]{};
return new ArrayList<>();
}
return Arrays.stream(drones)
return drones
.stream()
.filter(drone -> drone.capability().cooling() == state)
.map(Drone::id)
.toArray(String[]::new);
.collect(Collectors.toList());
}
/**
@ -88,10 +94,7 @@ public class DroneInfoService {
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
*/
public Drone droneDetail(String id) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
List<Drone> drones = fetchAllDrones();
if (drones == null) {
throw new NullPointerException("drone cannot be found");
@ -105,96 +108,8 @@ public class DroneInfoService {
// This will result in 404
throw new IllegalArgumentException(
"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);
"drone with that ID cannot be found"
);
}
/**
@ -207,27 +122,34 @@ public class DroneInfoService {
* @return array of drone ids that match all the requirements
* @see io.github.js0ny.ilp_coursework.controller.DroneController#queryAvailableDrones
*/
public String[] dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
Drone[] drones = restTemplate.getForObject(
droneUrl,
Drone[].class);
public List<String> dronesMatchesRequirements(MedDispatchRecRequest[] rec) {
List<Drone> drones = fetchAllDrones();
if (drones == null || rec == null || rec.length == 0) {
return new String[]{};
if (drones == null) {
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
*/
return Arrays.stream(drones)
.filter(drone -> drone != null && drone.capability() != null)
.filter(drone -> Arrays.stream(rec)
.filter(record -> record != null && record.requirements() != null)
// Every record must be met
.allMatch(record -> meetsRequirement(drone, record)))
return drones
.stream()
.filter(d -> d != null && d.capability() != null)
.filter(d ->
Arrays.stream(rec)
.filter(r -> r != null && r.requirements() != null)
.allMatch(r -> meetsRequirement(d, r))
)
.map(Drone::id)
.toArray(String[]::new);
.collect(Collectors.toList());
}
/**
@ -240,14 +162,16 @@ public class DroneInfoService {
* is invalid (capacity and id cannot be null
* in {@code MedDispathRecDto})
*/
private boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
public boolean meetsRequirement(Drone drone, MedDispatchRecRequest record) {
var requirements = record.requirements();
if (requirements == null) {
throw new IllegalArgumentException("requirements cannot be null");
}
var capability = drone.capability();
if (capability == null) {
throw new IllegalArgumentException("drone capability cannot be null");
throw new IllegalArgumentException(
"drone capability cannot be null"
);
}
float requiredCapacity = requirements.capacity();
@ -256,24 +180,24 @@ public class DroneInfoService {
}
// Use boolean wrapper to allow null (not specified) values
Boolean requiredCooling = requirements.cooling();
Boolean requiredHeating = requirements.heating();
Float requiredMaxCost = requirements.maxCost();
boolean requiredCooling = requirements.cooling();
boolean requiredHeating = requirements.heating();
boolean matchesCooling = requiredCooling == null || capability.cooling() == requiredCooling;
boolean matchesHeating = requiredHeating == null || capability.heating() == requiredHeating;
boolean matchesCost = false;
float totalCost = capability.costInitial() + capability.costFinal();
if (capability.maxMoves() > 0) {
totalCost += capability.costPerMove();
}
matchesCost = totalCost <= requiredMaxCost;
// Case 1: required is null: We don't care about it
// Case 2: required is false: We don't care about it (high capability adapts to low requirements)
// Case 3: capability is true: Then always matches
// See: https://piazza.com/class/me9vp64lfgf4sn/post/100
boolean matchesCooling = !requiredCooling || capability.cooling();
boolean matchesHeating = !requiredHeating || capability.heating();
// Conditions: All requirements matched + availability matched, use helper
// 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
* @return true if the drone is available, false otherwise
*/
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
private boolean checkAvailability(
String droneId,
MedDispatchRecRequest record
) {
URI droneUrl = URI.create(baseUrl).resolve(
dronesForServicePointsEndpoint
);
ServicePointDrones[] servicePoints = restTemplate.getForObject(
droneUrl,
ServicePointDrones[].class);
ServicePointDrones[].class
);
LocalDate requiredDate = record.date();
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
LocalTime requiredTime = record.time();
assert servicePoints != null;
for (var servicePoint : servicePoints) {
var drone = servicePoint.locateDroneById(droneId); // Nullable
if (drone != null) {
return drone.checkAvailability(requiredDay, requiredTime);
}
}
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;
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 java.util.List;
import org.springframework.stereotype.Service;
/**
@ -49,6 +48,11 @@ public class GpsCalculationService {
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
* {@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 Region#isClosed()
*/
public boolean checkIsInRegion(LngLat position, Region region) throws IllegalArgumentException {
if (!region.isClosed()) { // call method from RegionDto to check if not closed
public boolean checkIsInRegion(LngLat position, Region region)
throws IllegalArgumentException {
if (!region.isClosed()) {
// call method from RegionDto to check if not closed
throw new IllegalArgumentException("Region is not closed.");
}
return rayCasting(position, region.vertices());
@ -146,7 +152,10 @@ public class GpsCalculationService {
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()) {
++intersections;
@ -171,14 +180,19 @@ public class GpsCalculationService {
*/
private boolean isPointOnEdge(LngLat p, LngLat a, LngLat b) {
// Cross product: (p - a) × (b - a)
double crossProduct = (p.lng() - a.lng()) * (b.lat() - a.lat())
- (p.lat() - a.lat()) * (b.lng() - a.lng());
double crossProduct =
(p.lng() - a.lng()) * (b.lat() - a.lat()) -
(p.lat() - a.lat()) * (b.lng() - a.lng());
if (Math.abs(crossProduct) > 1e-9) {
return false;
}
boolean isWithinLng = p.lng() >= Math.min(a.lng(), b.lng()) && p.lng() <= Math.max(a.lng(), b.lng());
boolean isWithinLat = p.lat() >= Math.min(a.lat(), b.lat()) && p.lat() <= Math.max(a.lat(), b.lat());
boolean isWithinLng =
p.lng() >= Math.min(a.lng(), b.lng()) &&
p.lng() <= Math.max(a.lng(), b.lng());
boolean isWithinLat =
p.lat() >= Math.min(a.lat(), b.lat()) &&
p.lat() <= Math.max(a.lat(), b.lat());
return isWithinLng && isWithinLat;
}

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.RegionCheckRequest;
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@ -68,10 +66,7 @@ public class ApiControllerTest {
LngLat p2 = new LngLat(3.0, 0);
var req = new DistanceRequest(p1, p2);
when(
service.calculateDistance(
any(LngLat.class),
any(LngLat.class)
)
service.calculateDistance(any(LngLat.class), any(LngLat.class))
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
@ -228,10 +223,7 @@ public class ApiControllerTest {
);
var req = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenReturn(expected);
var mock = mockMvc.perform(
post(endpoint)
@ -253,10 +245,7 @@ public class ApiControllerTest {
var region = new Region("illegal", List.of());
var request = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
.perform(
@ -286,10 +275,7 @@ public class ApiControllerTest {
);
var request = new RegionCheckRequest(position, region);
when(
service.checkIsInRegion(
any(LngLat.class),
any(Region.class)
)
service.checkIsInRegion(any(LngLat.class), any(Region.class))
).thenThrow(new IllegalArgumentException("Region is not closed."));
mockMvc
.perform(

View file

@ -1,18 +1,17 @@
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.assertThatThrownBy;
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 {
private static final double STEP = 0.00015;
@ -29,6 +28,7 @@ public class GpsCalculationServiceTest {
@Nested
@DisplayName("Test for calculateDistance(LngLatDto, LngLatDto) -> double")
class CalculateDistanceTests {
@Test
@DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() {
@ -92,6 +92,7 @@ public class GpsCalculationServiceTest {
@Nested
@DisplayName("Test for isCloseTo(LngLatDto, LngLatDto) -> boolean")
class IsCloseToTests {
@Test
@DisplayName("False: Given Example For Testing")
void isCloseTo_shouldReturnFalse_givenExample() {
@ -112,7 +113,9 @@ public class GpsCalculationServiceTest {
}
@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() {
var p1 = new LngLat(0.0, 0.0);
var p2 = new LngLat(0.0, 0.00014);
@ -156,12 +159,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@DisplayName("Cardinal Direction: nextPosition in North direction (90 degrees)")
@DisplayName(
"Cardinal Direction: nextPosition in North direction (90 degrees)"
)
void nextPosition_shouldMoveNorth_forAngle90() {
var start = new LngLat(0.0, 0.0);
double angle = 90;
@ -170,12 +181,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@DisplayName("Cardinal Direction: nextPosition in West direction (180 degrees)")
@DisplayName(
"Cardinal Direction: nextPosition in West direction (180 degrees)"
)
void nextPosition_shouldMoveWest_forAngle180() {
var start = new LngLat(0.0, 0.0);
double angle = 180;
@ -185,12 +204,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@DisplayName("Cardinal Direction: nextPosition in South direction (270 degrees)")
@DisplayName(
"Cardinal Direction: nextPosition in South direction (270 degrees)"
)
void nextPosition_shouldMoveSouth_forAngle270() {
var start = new LngLat(0.0, 0.0);
double angle = 270;
@ -200,12 +227,20 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@DisplayName("Intercardinal Direction: nextPosition in Northeast direction (45 degrees)")
@DisplayName(
"Intercardinal Direction: nextPosition in Northeast direction (45 degrees)"
)
void nextPosition_shouldMoveNortheast_forAngle45() {
var start = new LngLat(0.0, 0.0);
double angle = 45;
@ -216,8 +251,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@ -227,14 +268,22 @@ public class GpsCalculationServiceTest {
// 405 degrees is equivalent to 45 degrees (405 % 360 = 45).
double angle = 405;
double equivalentAngle = 45;
double expectedLng = STEP * Math.cos(Math.toRadians(equivalentAngle));
double expectedLat = STEP * Math.sin(Math.toRadians(equivalentAngle));
double expectedLng =
STEP * Math.cos(Math.toRadians(equivalentAngle));
double expectedLat =
STEP * Math.sin(Math.toRadians(equivalentAngle));
var expected = new LngLat(expectedLng, expectedLat);
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
expected.lng(),
within(PRECISION)
);
assertThat(actual.lat()).isCloseTo(
expected.lat(),
within(PRECISION)
);
}
@Test
@ -249,8 +298,14 @@ public class GpsCalculationServiceTest {
var actual = service.nextPosition(start, angle);
assertThat(actual.lng()).isCloseTo(expected.lng(), within(PRECISION));
assertThat(actual.lat()).isCloseTo(expected.lat(), within(PRECISION));
assertThat(actual.lng()).isCloseTo(
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")
class CheckIsInRegionTests {
public static final Region RECTANGLE_REGION = new Region("rectangle", List.of(new LngLat(0.0, 0.0),
new LngLat(2.0, 0.0), new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, 0.0)));
public static final Region RECTANGLE_REGION = new Region(
"rectangle",
List.of(
new LngLat(0.0, 0.0),
new LngLat(2.0, 0.0),
new LngLat(2.0, 2.0),
new LngLat(0.0, 2.0),
new LngLat(0.0, 0.0)
)
);
@Test
@DisplayName("General Case: Given Example for Testing")
void isInRegion_shouldReturnFalse_givenPolygonCentral() {
var position = new LngLat(1.234, 1.222);
var region = new Region("central",
List.of(new LngLat(-3.192473, 55.946233), new LngLat(-3.192473, 55.942617),
new LngLat(-3.184319, 55.942617), new LngLat(-3.184319, 55.946233),
new LngLat(-3.192473, 55.946233)));
var region = new Region(
"central",
List.of(
new LngLat(-3.192473, 55.946233),
new LngLat(-3.192473, 55.942617),
new LngLat(-3.184319, 55.942617),
new LngLat(-3.184319, 55.946233),
new LngLat(-3.192473, 55.946233)
)
);
boolean expected = false;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@ -279,7 +348,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_forSimpleRectangle() {
var position = new LngLat(1.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -288,7 +360,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnFalse_forSimpleRectangle() {
var position = new LngLat(3.0, 1.0);
boolean expected = false;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -296,10 +371,18 @@ public class GpsCalculationServiceTest {
@DisplayName("General Case: Simple Hexagon")
void isInRegion_shouldReturnTrue_forSimpleHexagon() {
var position = new LngLat(2.0, 2.0);
var region = new Region("hexagon",
List.of(new LngLat(1.0, 0.0), new LngLat(4.0, 0.0), new LngLat(5.0, 2.0),
new LngLat(4.0, 4.0), new LngLat(1.0, 4.0), new LngLat(0.0, 2.0),
new LngLat(1.0, 0.0)));
var region = new Region(
"hexagon",
List.of(
new LngLat(1.0, 0.0),
new LngLat(4.0, 0.0),
new LngLat(5.0, 2.0),
new LngLat(4.0, 4.0),
new LngLat(1.0, 4.0),
new LngLat(0.0, 2.0),
new LngLat(1.0, 0.0)
)
);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@ -309,8 +392,15 @@ public class GpsCalculationServiceTest {
@DisplayName("Edge Case: Small Triangle")
void isInRegion_shouldReturnTrue_forSmallTriangle() {
var position = new LngLat(0.00001, 0.00001);
var region = new Region("triangle", List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0),
new LngLat(0.00005, 0.0001), new LngLat(0.0, 0.0)));
var region = new Region(
"triangle",
List.of(
new LngLat(0.0, 0.0),
new LngLat(0.0001, 0.0),
new LngLat(0.00005, 0.0001),
new LngLat(0.0, 0.0)
)
);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, region);
assertThat(actual).isEqualTo(expected);
@ -321,7 +411,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerEdge() {
var position = new LngLat(0.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -330,7 +423,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperEdge() {
var position = new LngLat(2.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -339,7 +435,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLeftEdge() {
var position = new LngLat(0.0, 1.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -348,7 +447,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnLowerVertex() {
var position = new LngLat(0.0, 0.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -357,7 +459,10 @@ public class GpsCalculationServiceTest {
void isInRegion_shouldReturnTrue_whenPointOnUpperVertex() {
var position = new LngLat(2.0, 2.0);
boolean expected = true;
boolean actual = service.checkIsInRegion(position, RECTANGLE_REGION);
boolean actual = service.checkIsInRegion(
position,
RECTANGLE_REGION
);
assertThat(actual).isEqualTo(expected);
}
@ -365,22 +470,40 @@ public class GpsCalculationServiceTest {
@DisplayName("Edge Case: Region not forming polygon")
void isInRegion_shouldThrowExceptions_whenRegionNotFormingPolygon() {
var position = new LngLat(2.0, 2.0);
var region = new Region("line",
List.of(new LngLat(0.0, 0.0), new LngLat(0.0001, 0.0), new LngLat(0.0, 0.0)));
var region = new Region(
"line",
List.of(
new LngLat(0.0, 0.0),
new LngLat(0.0001, 0.0),
new LngLat(0.0, 0.0)
)
);
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Region is not closed.");
}
@Test
@DisplayName("Edge Case: Region is not closed")
void isInRegion_shouldThrowExceptions_whenRegionNotClose() {
var position = new LngLat(2.0, 2.0);
var region = new Region("rectangle", List.of(new LngLat(0.0, 0.0), new LngLat(2.0, 0.0),
new LngLat(2.0, 2.0), new LngLat(0.0, 2.0), new LngLat(0.0, -1.0)));
var region = new Region(
"rectangle",
List.of(
new LngLat(0.0, 0.0),
new LngLat(2.0, 0.0),
new LngLat(2.0, 2.0),
new LngLat(0.0, 2.0),
new LngLat(0.0, -1.0)
)
);
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Region is not closed.");
}
@Test
@ -390,7 +513,9 @@ public class GpsCalculationServiceTest {
var region = new Region("rectangle", List.of());
assertThatThrownBy(() -> {
service.checkIsInRegion(position, region);
}).isInstanceOf(IllegalArgumentException.class).hasMessage("Region is not closed.");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Region is not closed.");
}
}
}