refractor(controller): Use List<> instead of []
This commit is contained in:
parent
69d9e0d736
commit
141a957a8d
24 changed files with 991 additions and 380 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -39,3 +39,4 @@ out/
|
|||
*.tar
|
||||
.direnv/
|
||||
.envrc
|
||||
localjson
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=linux/amd64 openjdk:21
|
||||
FROM maven:3.9.9-amazoncorretto-21-debian AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ body:json {
|
|||
"cooling": false,
|
||||
"heating": true,
|
||||
"maxCost": 13.5
|
||||
},
|
||||
"delivery": {
|
||||
"lng": -3.00,
|
||||
"lat": 55.121
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ body:json {
|
|||
"capacity": 0.75,
|
||||
"heating": true,
|
||||
"maxCost": 13.5
|
||||
}
|
||||
},
|
||||
"delivery": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
@PathVariable String attrName,
|
||||
@PathVariable String attrVal) {
|
||||
return droneService.dronesWithAttribute(attrName, attrVal);
|
||||
public List<String> getIdByAttrMap(
|
||||
@PathVariable String attrName,
|
||||
@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 "{}";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package io.github.js0ny.ilp_coursework.data.common;
|
||||
|
||||
public record DronePathDto() {
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package io.github.js0ny.ilp_coursework.data.common;
|
||||
|
||||
public record MedRequirement(
|
||||
float capacity,
|
||||
boolean cooling,
|
||||
boolean heating,
|
||||
float maxCost
|
||||
) {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
23
src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
vendored
Normal file
23
src/main/java/io/github/js0ny/ilp_coursework/data/external/RestrictedArea.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
5
src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
vendored
Normal file
5
src/main/java/io/github/js0ny/ilp_coursework/data/external/ServicePoint.java
vendored
Normal 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) {}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
float totalCost,
|
||||
int totalMoves,
|
||||
DronePath[] dronePaths
|
||||
) {
|
||||
public record DronePath(int droneId, List<Delivery> deliveries) {
|
||||
public record Delivery(int deliveryId, List<LngLat> flightPath) {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
.filter(drone -> drone.capability().cooling() == state)
|
||||
.map(Drone::id)
|
||||
.toArray(String[]::new);
|
||||
return drones
|
||||
.stream()
|
||||
.filter(drone -> drone.capability().cooling() == state)
|
||||
.map(Drone::id)
|
||||
.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)))
|
||||
.map(Drone::id)
|
||||
.toArray(String[]::new);
|
||||
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)
|
||||
.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);
|
||||
droneUrl,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -61,22 +59,19 @@ public class ApiControllerTest {
|
|||
@Test
|
||||
@DisplayName("POST /distanceTo -> 200 OK")
|
||||
void getDistance_shouldReturn200AndDistance_whenCorrectInput()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
double expected = 5.0;
|
||||
String endpoint = "/api/v1/distanceTo";
|
||||
LngLat p1 = new LngLat(0, 4.0);
|
||||
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)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
);
|
||||
|
||||
mock.andExpect(status().isOk());
|
||||
|
|
@ -88,23 +83,23 @@ public class ApiControllerTest {
|
|||
void getDistance_shouldReturn400_whenMissingField() throws Exception {
|
||||
String endpoint = "/api/v1/distanceTo";
|
||||
String req = """
|
||||
{
|
||||
"position1": {
|
||||
"lng": 3.0,
|
||||
"lat": 4.0
|
||||
}
|
||||
{
|
||||
"position1": {
|
||||
"lng": 3.0,
|
||||
"lat": 4.0
|
||||
}
|
||||
""";
|
||||
}
|
||||
""";
|
||||
when(
|
||||
service.calculateDistance(any(LngLat.class), isNull())
|
||||
service.calculateDistance(any(LngLat.class), isNull())
|
||||
).thenThrow(new NullPointerException());
|
||||
mockMvc
|
||||
.perform(
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(req)
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
.perform(
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(req)
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,19 +110,19 @@ public class ApiControllerTest {
|
|||
@Test
|
||||
@DisplayName("POST /isCloseTo -> 200 OK")
|
||||
void getIsCloseTo_shouldReturn200AndBoolean_whenCorrectInput()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
boolean expected = false;
|
||||
String endpoint = "/api/v1/isCloseTo";
|
||||
LngLat p1 = new LngLat(0, 4.0);
|
||||
LngLat p2 = new LngLat(3.0, 0);
|
||||
var req = new DistanceRequest(p1, p2);
|
||||
when(
|
||||
service.isCloseTo(any(LngLat.class), any(LngLat.class))
|
||||
service.isCloseTo(any(LngLat.class), any(LngLat.class))
|
||||
).thenReturn(expected);
|
||||
var mock = mockMvc.perform(
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
);
|
||||
|
||||
mock.andExpect(status().isOk());
|
||||
|
|
@ -137,19 +132,19 @@ public class ApiControllerTest {
|
|||
@Test
|
||||
@DisplayName("POST /isCloseTo -> 400 Bad Request: Malformed JSON ")
|
||||
void getIsCloseTo_shouldReturn400_whenJsonIsMalformed()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
// json without a bracket
|
||||
String malformedJson = """
|
||||
{
|
||||
"position1": { "lng": 0.0, "lat": 3.0 }
|
||||
""";
|
||||
{
|
||||
"position1": { "lng": 0.0, "lat": 3.0 }
|
||||
""";
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/v1/isCloseTo")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(malformedJson)
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
.perform(
|
||||
post("/api/v1/isCloseTo")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(malformedJson)
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,46 +157,46 @@ public class ApiControllerTest {
|
|||
@Test
|
||||
@DisplayName("POST /nextPosition -> 200 OK")
|
||||
void getNextPosition_shouldReturn200AndCoordinate_whenCorrectInput()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
LngLat expected = new LngLat(0.00015, 0.0);
|
||||
LngLat p = new LngLat(0, 0);
|
||||
var req = new MovementRequest(p, 0);
|
||||
when(
|
||||
service.nextPosition(any(LngLat.class), anyDouble())
|
||||
service.nextPosition(any(LngLat.class), anyDouble())
|
||||
).thenReturn(expected);
|
||||
var mock = mockMvc.perform(
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
);
|
||||
|
||||
mock.andExpect(status().isOk());
|
||||
mock.andExpect(
|
||||
content().json(objectMapper.writeValueAsString(expected))
|
||||
content().json(objectMapper.writeValueAsString(expected))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /nextPosition -> 400 Bad Request: Missing Field")
|
||||
void getNextPosition_shouldReturn400_whenKeyNameError()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
// "position" should be "start"
|
||||
String malformedJson = """
|
||||
{
|
||||
"position": { "lng": 0.0, "lat": 3.0 },
|
||||
"angle": 180
|
||||
}
|
||||
""";
|
||||
{
|
||||
"position": { "lng": 0.0, "lat": 3.0 },
|
||||
"angle": 180
|
||||
}
|
||||
""";
|
||||
when(service.nextPosition(isNull(), anyDouble())).thenThrow(
|
||||
new NullPointerException()
|
||||
new NullPointerException()
|
||||
);
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/v1/nextPosition")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(malformedJson)
|
||||
)
|
||||
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
||||
.perform(
|
||||
post("/api/v1/nextPosition")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(malformedJson)
|
||||
)
|
||||
.andExpect(MockMvcResultMatchers.status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,31 +207,28 @@ public class ApiControllerTest {
|
|||
@Test
|
||||
@DisplayName("POST /isInRegion -> 200 OK")
|
||||
void getIsInRegion_shouldReturn200AndBoolean_whenCorrectInput()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
boolean expected = false;
|
||||
String endpoint = "/api/v1/isInRegion";
|
||||
var position = new 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)
|
||||
)
|
||||
"central",
|
||||
List.of(
|
||||
new LngLat(-3.192473, 55.946233),
|
||||
new LngLat(-3.192473, 55.942617),
|
||||
new LngLat(-3.184319, 55.942617),
|
||||
new LngLat(-3.184319, 55.946233),
|
||||
new LngLat(-3.192473, 55.946233)
|
||||
)
|
||||
);
|
||||
var req = new 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)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
post(endpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
);
|
||||
|
||||
mock.andExpect(status().isOk());
|
||||
|
|
@ -245,59 +237,53 @@ public class ApiControllerTest {
|
|||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
|
||||
"POST /isInRegion -> 400 Bad Request: Passing a list of empty vertices to isInRegion"
|
||||
)
|
||||
void getIsInRegion_shouldReturn400_whenPassingIllegalArguments()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
var position = new LngLat(1, 1);
|
||||
var region = new Region("illegal", List.of());
|
||||
var request = new RegionCheckRequest(position, region);
|
||||
when(
|
||||
service.checkIsInRegion(
|
||||
any(LngLat.class),
|
||||
any(Region.class)
|
||||
)
|
||||
service.checkIsInRegion(any(LngLat.class), any(Region.class))
|
||||
).thenThrow(new IllegalArgumentException("Region is not closed."));
|
||||
mockMvc
|
||||
.perform(
|
||||
post("/api/v1/isInRegion")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
.perform(
|
||||
post("/api/v1/isInRegion")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
|
||||
"POST /isInRegion -> 400 Bad Request: Passing a list of not-closing vertices to isInRegion"
|
||||
)
|
||||
void getIsInRegion_shouldReturn400_whenPassingNotClosingVertices()
|
||||
throws Exception {
|
||||
throws Exception {
|
||||
var position = new LngLat(1, 1);
|
||||
var region = new Region(
|
||||
"illegal",
|
||||
List.of(
|
||||
new LngLat(1, 2),
|
||||
new LngLat(3, 4),
|
||||
new LngLat(5, 6),
|
||||
new LngLat(7, 8),
|
||||
new LngLat(9, 10)
|
||||
)
|
||||
"illegal",
|
||||
List.of(
|
||||
new LngLat(1, 2),
|
||||
new LngLat(3, 4),
|
||||
new LngLat(5, 6),
|
||||
new LngLat(7, 8),
|
||||
new LngLat(9, 10)
|
||||
)
|
||||
);
|
||||
var request = new 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(
|
||||
post("/api/v1/isInRegion")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
.perform(
|
||||
post("/api/v1/isInRegion")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
)
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue