ci(automation): pre-ci setup
This commit is contained in:
parent
15ad7a2fb7
commit
4e623fada6
11 changed files with 293 additions and 185 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -47,3 +47,6 @@ drone-black-box/drone-black-box
|
||||||
drone-black-box/drone_black_box.db*
|
drone-black-box/drone_black_box.db*
|
||||||
|
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|
||||||
|
*.out
|
||||||
|
*.html
|
||||||
|
|
|
||||||
14
.justfile
Normal file
14
.justfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
format:
|
||||||
|
cd ./drone-black-box && go fmt
|
||||||
|
fd --extension java --exec google-java-format --replace --aosp {}
|
||||||
|
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
cd ./drone-black-box && go vet ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
cd ./drone-black-box && go test -v ./... -coverprofile=coverage.out
|
||||||
|
cd ./drone-black-box && go tool cover -html=coverage.out -o coverage.html
|
||||||
|
rm ./drone-black-box/coverage.out
|
||||||
|
# Java
|
||||||
|
cd ./ilp-rest-service/ && ./gradlew check
|
||||||
|
|
@ -125,6 +125,7 @@ func TestIngestBadJSON(t *testing.T) {
|
||||||
// Ensure graceful shutdown path does not hang: start a server and shut it down quickly.
|
// Ensure graceful shutdown path does not hang: start a server and shut it down quickly.
|
||||||
func TestGracefulShutdown(t *testing.T) {
|
func TestGracefulShutdown(t *testing.T) {
|
||||||
db := newTestDB(t)
|
db := newTestDB(t)
|
||||||
|
defer db.Close()
|
||||||
srv := &Server{db: db}
|
srv := &Server{db: db}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /health", srv.healthHandler)
|
mux.HandleFunc("GET /health", srv.healthHandler)
|
||||||
|
|
@ -138,3 +139,75 @@ func TestGracefulShutdown(t *testing.T) {
|
||||||
t.Fatalf("shutdown: %v", err)
|
t.Fatalf("shutdown: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCORSAllowedOriginAndPreflight(t *testing.T) {
|
||||||
|
handled := false
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handled = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
handler := corsMiddleware(next)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
req.Header.Set("Origin", "http://localhost:5173")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" {
|
||||||
|
t.Fatalf("expected allow-origin header, got %q", got)
|
||||||
|
}
|
||||||
|
if !handled {
|
||||||
|
t.Fatalf("expected handler to be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight := httptest.NewRequest(http.MethodOptions, "/health", nil)
|
||||||
|
preflight.Header.Set("Origin", "http://localhost:5173")
|
||||||
|
preflightRec := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(preflightRec, preflight)
|
||||||
|
if preflightRec.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("preflight expected 204, got %d", preflightRec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngestDBError(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
db.Close()
|
||||||
|
srv := &Server{db: db}
|
||||||
|
|
||||||
|
ev := DroneEvent{DroneID: "d1", Latitude: 1.0, Longitude: 2.0, Timestamp: "2025-12-06T00:00:00Z"}
|
||||||
|
body, _ := json.Marshal(ev)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/ingest", bytes.NewReader(body))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ingestHandler(rec, req)
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("expected 500, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshotQueryError(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
db.Close()
|
||||||
|
srv := &Server{db: db}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/snapshot?time=2025-12-06T00:00:00Z", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.snapshotHandler(rec, req)
|
||||||
|
if rec.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("expected 500, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealthFailure(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
db.Close()
|
||||||
|
srv := &Server{db: db}
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.healthHandler(rec, req)
|
||||||
|
if rec.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("expected 503, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.5.6'
|
id 'org.springframework.boot' version '3.5.6'
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'io.github.js0ny'
|
group = 'io.github.js0ny'
|
||||||
|
|
@ -26,6 +27,56 @@ dependencies {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion = "0.8.12"
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
finalizedBy jacocoTestReport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jacocoTestReport {
|
||||||
|
dependsOn test
|
||||||
|
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||||
|
fileTree(dir: it, exclude: [
|
||||||
|
'**/IlpCourseworkApplication.class',
|
||||||
|
'**/config/*',
|
||||||
|
'**/data/*',
|
||||||
|
'**/util/*'
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jacocoTestCoverageVerification {
|
||||||
|
violationRules {
|
||||||
|
rule {
|
||||||
|
element = 'CLASS'
|
||||||
|
|
||||||
|
excludes = [
|
||||||
|
'io.github.js0ny.IlpCourseworkApplication',
|
||||||
|
'**.config.**',
|
||||||
|
'**.data.**',
|
||||||
|
'**.util.**'
|
||||||
|
]
|
||||||
|
|
||||||
|
limit {
|
||||||
|
counter = 'BRANCH'
|
||||||
|
value = 'COVEREDRATIO'
|
||||||
|
|
||||||
|
// minimum = 0.50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
check.dependsOn jacocoTestCoverageVerification
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ body:json {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
"date": "2025-12-22",
|
"date": "2025-12-25",
|
||||||
"time": "14:30",
|
"time": "14:30",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"capacity": 0.75,
|
"capacity": 0.75,
|
||||||
|
|
@ -28,7 +28,7 @@ body:json {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 456,
|
"id": 456,
|
||||||
"date": "2025-12-25",
|
"date": "2025-12-23",
|
||||||
"time": "11:30",
|
"time": "11:30",
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"capacity": 0.75,
|
"capacity": 0.75,
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,17 @@ import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
/**
|
/** Global CORS configuration so the frontend running on a different port can call the REST API. */
|
||||||
* Global CORS configuration so the frontend running on a different port can call the REST API.
|
|
||||||
*/
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class CorsConfig implements WebMvcConfigurer {
|
public class CorsConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
private static final String[] ALLOWED_ORIGINS = new String[] {
|
private static final String[] ALLOWED_ORIGINS =
|
||||||
"http://localhost:4173",
|
new String[] {
|
||||||
"http://127.0.0.1:4173",
|
"http://localhost:4173",
|
||||||
"http://localhost:5173",
|
"http://127.0.0.1:4173",
|
||||||
"http://127.0.0.1:5173"
|
"http://localhost:5173",
|
||||||
};
|
"http://127.0.0.1:5173"
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
package io.github.js0ny.ilp_coursework.controller;
|
||||||
|
|
||||||
import java.util.List;
|
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
||||||
|
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
||||||
|
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
import java.util.List;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
|
||||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package io.github.js0ny.ilp_coursework.data.common;
|
package io.github.js0ny.ilp_coursework.data.common;
|
||||||
|
|
||||||
|
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
|
||||||
|
|
||||||
// Corresponding in Go
|
// Corresponding in Go
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
@ -18,13 +18,10 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public record DroneEvent(
|
public record DroneEvent(String droneId, double latitude, double longitude, String timestamp) {
|
||||||
String droneId,
|
|
||||||
double latitude,
|
static final int STEP = 1; // seconds between events
|
||||||
double longitude,
|
|
||||||
String timestamp) {
|
|
||||||
|
|
||||||
final static int STEP = 1; // seconds between events
|
|
||||||
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
||||||
|
|
||||||
public static List<DroneEvent> fromPathResponse(DeliveryPathResponse resp) {
|
public static List<DroneEvent> fromPathResponse(DeliveryPathResponse resp) {
|
||||||
|
|
@ -34,11 +31,7 @@ public record DroneEvent(
|
||||||
for (var d : p.deliveries()) {
|
for (var d : p.deliveries()) {
|
||||||
for (var coord : d.flightPath()) {
|
for (var coord : d.flightPath()) {
|
||||||
String timestamp = java.time.Instant.now().toString();
|
String timestamp = java.time.Instant.now().toString();
|
||||||
events.add(new DroneEvent(
|
events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp));
|
||||||
id,
|
|
||||||
coord.lat(),
|
|
||||||
coord.lng(),
|
|
||||||
timestamp));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,19 +40,15 @@ public record DroneEvent(
|
||||||
|
|
||||||
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
||||||
// with base timestamp
|
// with base timestamp
|
||||||
public static List<DroneEvent> fromPathResponseWithTimestamp(DeliveryPathResponse resp,
|
public static List<DroneEvent> fromPathResponseWithTimestamp(
|
||||||
LocalDateTime baseTimestamp) {
|
DeliveryPathResponse resp, LocalDateTime baseTimestamp) {
|
||||||
List<DroneEvent> events = new java.util.ArrayList<>();
|
List<DroneEvent> events = new java.util.ArrayList<>();
|
||||||
java.time.LocalDateTime timestamp = baseTimestamp;
|
java.time.LocalDateTime timestamp = baseTimestamp;
|
||||||
for (var p : resp.dronePaths()) {
|
for (var p : resp.dronePaths()) {
|
||||||
String id = String.valueOf(p.droneId());
|
String id = String.valueOf(p.droneId());
|
||||||
for (var d : p.deliveries()) {
|
for (var d : p.deliveries()) {
|
||||||
for (var coord : d.flightPath()) {
|
for (var coord : d.flightPath()) {
|
||||||
events.add(new DroneEvent(
|
events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp.toString()));
|
||||||
id,
|
|
||||||
coord.lat(),
|
|
||||||
coord.lng(),
|
|
||||||
timestamp.toString()));
|
|
||||||
timestamp = timestamp.plusSeconds(STEP); // Increment timestamp for each event
|
timestamp = timestamp.plusSeconds(STEP); // Increment timestamp for each event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,14 +64,11 @@ public record DroneEvent(
|
||||||
for (var d : p.deliveries()) {
|
for (var d : p.deliveries()) {
|
||||||
LocalDateTime timestamp = deliveryTimestamps.get(d.deliveryId());
|
LocalDateTime timestamp = deliveryTimestamps.get(d.deliveryId());
|
||||||
// Fallback to current time if the delivery does not carry a timestamp.
|
// Fallback to current time if the delivery does not carry a timestamp.
|
||||||
System.out.println("Generated event for drone " + id + " at " + timestamp.toString());
|
System.out.println(
|
||||||
|
"Generated event for drone " + id + " at " + timestamp.toString());
|
||||||
LocalDateTime current = timestamp != null ? timestamp : LocalDateTime.now();
|
LocalDateTime current = timestamp != null ? timestamp : LocalDateTime.now();
|
||||||
for (var coord : d.flightPath()) {
|
for (var coord : d.flightPath()) {
|
||||||
events.add(new DroneEvent(
|
events.add(new DroneEvent(id, coord.lat(), coord.lng(), current.toString()));
|
||||||
id,
|
|
||||||
coord.lat(),
|
|
||||||
coord.lng(),
|
|
||||||
current.toString()));
|
|
||||||
current = current.plusSeconds(STEP);
|
current = current.plusSeconds(STEP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||||
import io.github.js0ny.ilp_coursework.data.common.Region;
|
|
||||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
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.RestrictedArea;
|
||||||
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
||||||
|
|
@ -54,15 +51,13 @@ public class DroneInfoService {
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones with/without cooling capability
|
* Return an array of ids of drones with/without cooling capability
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>Associated service method with {@code /dronesWithCooling/{state}}
|
||||||
* Associated service method with {@code /dronesWithCooling/{state}}
|
|
||||||
*
|
*
|
||||||
* @param state determines the capability filtering
|
* @param state determines the capability filtering
|
||||||
* @return if {@code state} is true, return ids of drones with cooling
|
* @return if {@code state} is true, return ids of drones with cooling capability, else without
|
||||||
* capability, else without
|
* cooling
|
||||||
* cooling
|
|
||||||
* @see
|
* @see
|
||||||
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||||
*/
|
*/
|
||||||
public List<String> dronesWithCooling(boolean state) {
|
public List<String> dronesWithCooling(boolean state) {
|
||||||
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||||
|
|
@ -82,16 +77,13 @@ public class DroneInfoService {
|
||||||
/**
|
/**
|
||||||
* Return a {@link Drone}-style json data structure with the given {@code id}
|
* Return a {@link Drone}-style json data structure with the given {@code id}
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>Associated service method with {@code /droneDetails/{id}}
|
||||||
* Associated service method with {@code /droneDetails/{id}}
|
|
||||||
*
|
*
|
||||||
* @param id The id of the drone
|
* @param id The id of the drone
|
||||||
* @return drone json body of given id
|
* @return drone json body of given id
|
||||||
* @throws NullPointerException when cannot fetch available drones from
|
* @throws NullPointerException when cannot fetch available drones from remote
|
||||||
* remote
|
* @throws IllegalArgumentException when drone with given {@code id} cannot be found this should
|
||||||
* @throws IllegalArgumentException when drone with given {@code id} cannot be
|
* lead to a 404
|
||||||
* found this should
|
|
||||||
* lead to a 404
|
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
||||||
*/
|
*/
|
||||||
public Drone droneDetail(String id) {
|
public Drone droneDetail(String id) {
|
||||||
|
|
@ -108,12 +100,10 @@ public class DroneInfoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones that match all the requirements in the
|
* Return an array of ids of drones that match all the requirements in the medical dispatch
|
||||||
* medical dispatch
|
|
||||||
* records
|
* records
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>Associated service method with
|
||||||
* Associated service method with
|
|
||||||
*
|
*
|
||||||
* @param rec array of medical dispatch records
|
* @param rec array of medical dispatch records
|
||||||
* @return List of drone ids that match all the requirements
|
* @return List of drone ids that match all the requirements
|
||||||
|
|
@ -135,9 +125,10 @@ public class DroneInfoService {
|
||||||
return drones.stream()
|
return drones.stream()
|
||||||
.filter(d -> d != null && d.capability() != null)
|
.filter(d -> d != null && d.capability() != null)
|
||||||
.filter(
|
.filter(
|
||||||
d -> Arrays.stream(rec)
|
d ->
|
||||||
.filter(r -> r != null && r.requirements() != null)
|
Arrays.stream(rec)
|
||||||
.allMatch(r -> droneMatchesRequirement(d, r)))
|
.filter(r -> r != null && r.requirements() != null)
|
||||||
|
.allMatch(r -> droneMatchesRequirement(d, r)))
|
||||||
.map(Drone::id)
|
.map(Drone::id)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
@ -145,13 +136,11 @@ public class DroneInfoService {
|
||||||
/**
|
/**
|
||||||
* Helper to check if a drone meets the requirement of a medical dispatch.
|
* Helper to check if a drone meets the requirement of a medical dispatch.
|
||||||
*
|
*
|
||||||
* @param drone the drone to be checked
|
* @param drone the drone to be checked
|
||||||
* @param record the medical dispatch record containing the requirement
|
* @param record the medical dispatch record containing the requirement
|
||||||
* @return true if the drone meets the requirement, false otherwise
|
* @return true if the drone meets the requirement, false otherwise
|
||||||
* @throws IllegalArgumentException when record requirements or drone capability
|
* @throws IllegalArgumentException when record requirements or drone capability is invalid
|
||||||
* is invalid
|
* (capacity and id cannot be null in {@code MedDispathRecDto})
|
||||||
* (capacity and id cannot be null in
|
|
||||||
* {@code MedDispathRecDto})
|
|
||||||
*/
|
*/
|
||||||
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
||||||
var requirements = record.requirements();
|
var requirements = record.requirements();
|
||||||
|
|
@ -191,13 +180,13 @@ public class DroneInfoService {
|
||||||
* Helper to check if a drone is available at the required date and time
|
* Helper to check if a drone is available at the required date and time
|
||||||
*
|
*
|
||||||
* @param droneId the id of the drone to be checked
|
* @param droneId the id of the drone to be checked
|
||||||
* @param record the medical dispatch record containing the required date and
|
* @param record the medical dispatch record containing the required date and time
|
||||||
* time
|
|
||||||
* @return true if the drone is available, false otherwise
|
* @return true if the drone is available, false otherwise
|
||||||
*/
|
*/
|
||||||
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
ServicePointDrones[] servicePoints =
|
||||||
|
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
|
|
||||||
LocalDate requiredDate = record.date();
|
LocalDate requiredDate = record.date();
|
||||||
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
||||||
|
|
@ -216,7 +205,8 @@ public class DroneInfoService {
|
||||||
|
|
||||||
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
ServicePointDrones[] servicePoints =
|
||||||
|
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -233,7 +223,8 @@ public class DroneInfoService {
|
||||||
private LngLat queryServicePointLocation(int id) {
|
private LngLat queryServicePointLocation(int id) {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
|
|
||||||
ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
ServicePoint[] servicePoints =
|
||||||
|
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -256,22 +247,24 @@ public class DroneInfoService {
|
||||||
|
|
||||||
public List<RestrictedArea> fetchRestrictedAreas() {
|
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||||
URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
|
URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
|
||||||
RestrictedArea[] restrictedAreas = restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
|
RestrictedArea[] restrictedAreas =
|
||||||
|
restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
|
||||||
assert restrictedAreas != null;
|
assert restrictedAreas != null;
|
||||||
return Arrays.asList(restrictedAreas);
|
return Arrays.asList(restrictedAreas);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServicePoint> fetchServicePoints() {
|
public List<ServicePoint> fetchServicePoints() {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
ServicePoint[] servicePoints =
|
||||||
|
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
return Arrays.asList(servicePoints);
|
return Arrays.asList(servicePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||||
URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePointDrones = restTemplate.getForObject(servicePointDronesUrl,
|
ServicePointDrones[] servicePointDrones =
|
||||||
ServicePointDrones[].class);
|
restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class);
|
||||||
assert servicePointDrones != null;
|
assert servicePointDrones != null;
|
||||||
return Arrays.asList(servicePointDrones);
|
return Arrays.asList(servicePointDrones);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePa
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -31,7 +32,6 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles calculations about deliverypath
|
* Class that handles calculations about deliverypath
|
||||||
|
|
@ -44,10 +44,8 @@ import java.time.LocalDateTime;
|
||||||
public class PathFinderService {
|
public class PathFinderService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard stop on how many pathfinding iterations we attempt for a single segment
|
* Hard stop on how many pathfinding iterations we attempt for a single segment before bailing,
|
||||||
* before bailing,
|
* useful for preventing infinite loops caused by precision quirks or unexpected map data.
|
||||||
* useful for preventing infinite loops caused by precision quirks or unexpected
|
|
||||||
* map data.
|
|
||||||
*
|
*
|
||||||
* @see #computePath(LngLat, LngLat)
|
* @see #computePath(LngLat, LngLat)
|
||||||
*/
|
*/
|
||||||
|
|
@ -67,14 +65,11 @@ public class PathFinderService {
|
||||||
private TelemetryService telemetryService;
|
private TelemetryService telemetryService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for PathFinderService. The dependencies are injected by Spring
|
* Constructor for PathFinderService. The dependencies are injected by Spring and the
|
||||||
* and the
|
* constructor pre-computes reference maps used throughout the request lifecycle.
|
||||||
* constructor pre-computes reference maps used throughout the request
|
|
||||||
* lifecycle.
|
|
||||||
*
|
*
|
||||||
* @param gpsCalculationService Service handling geometric operations.
|
* @param gpsCalculationService Service handling geometric operations.
|
||||||
* @param droneInfoService Service that exposes drone metadata and
|
* @param droneInfoService Service that exposes drone metadata and capability information.
|
||||||
* capability information.
|
|
||||||
*/
|
*/
|
||||||
public PathFinderService(
|
public PathFinderService(
|
||||||
GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) {
|
GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) {
|
||||||
|
|
@ -87,7 +82,8 @@ public class PathFinderService {
|
||||||
|
|
||||||
this.drones = droneInfoService.fetchAllDrones();
|
this.drones = droneInfoService.fetchAllDrones();
|
||||||
List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();
|
List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();
|
||||||
List<ServicePointDrones> servicePointAssignments = droneInfoService.fetchDronesForServicePoints();
|
List<ServicePointDrones> servicePointAssignments =
|
||||||
|
droneInfoService.fetchDronesForServicePoints();
|
||||||
List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();
|
List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();
|
||||||
|
|
||||||
this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone));
|
this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone));
|
||||||
|
|
@ -105,17 +101,17 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.servicePointLocations = servicePoints.stream()
|
this.servicePointLocations =
|
||||||
.collect(
|
servicePoints.stream()
|
||||||
Collectors.toMap(
|
.collect(
|
||||||
ServicePoint::id, sp -> new LngLat(sp.location())));
|
Collectors.toMap(
|
||||||
|
ServicePoint::id, sp -> new LngLat(sp.location())));
|
||||||
|
|
||||||
this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList();
|
this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produce a delivery plan for the provided dispatch records. Deliveries are
|
* Produce a delivery plan for the provided dispatch records. Deliveries are grouped per
|
||||||
* grouped per
|
|
||||||
* compatible drone and per trip to satisfy each drone move limit.
|
* compatible drone and per trip to satisfy each drone move limit.
|
||||||
*
|
*
|
||||||
* @param records Dispatch records to be fulfilled.
|
* @param records Dispatch records to be fulfilled.
|
||||||
|
|
@ -161,14 +157,17 @@ public class PathFinderService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MedDispatchRecRequest> sortedDeliveries = entry.getValue().stream()
|
List<MedDispatchRecRequest> sortedDeliveries =
|
||||||
.sorted(
|
entry.getValue().stream()
|
||||||
Comparator.comparingDouble(
|
.sorted(
|
||||||
rec -> gpsCalculationService.calculateDistance(
|
Comparator.comparingDouble(
|
||||||
servicePointLocation, rec.delivery())))
|
rec ->
|
||||||
.toList();
|
gpsCalculationService.calculateDistance(
|
||||||
|
servicePointLocation, rec.delivery())))
|
||||||
|
.toList();
|
||||||
|
|
||||||
List<List<MedDispatchRecRequest>> trips = splitTrips(sortedDeliveries, drone, servicePointLocation);
|
List<List<MedDispatchRecRequest>> trips =
|
||||||
|
splitTrips(sortedDeliveries, drone, servicePointLocation);
|
||||||
|
|
||||||
for (List<MedDispatchRecRequest> trip : trips) {
|
for (List<MedDispatchRecRequest> trip : trips) {
|
||||||
TripResult result = buildTrip(drone, servicePointLocation, trip);
|
TripResult result = buildTrip(drone, servicePointLocation, trip);
|
||||||
|
|
@ -246,10 +245,8 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group dispatch records by their assigned drone, ensuring every record is
|
* Group dispatch records by their assigned drone, ensuring every record is routed through
|
||||||
* routed through
|
* {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries.
|
||||||
* {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding
|
|
||||||
* invalid entries.
|
|
||||||
*
|
*
|
||||||
* @param records Dispatch records to be grouped.
|
* @param records Dispatch records to be grouped.
|
||||||
* @return Map keyed by drone ID with the deliveries it should service.
|
* @return Map keyed by drone ID with the deliveries it should service.
|
||||||
|
|
@ -268,8 +265,7 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Choose the best drone for the provided record. Currently that equates to
|
* Choose the best drone for the provided record. Currently that equates to picking the closest
|
||||||
* picking the closest
|
|
||||||
* compatible drone to the delivery location.
|
* compatible drone to the delivery location.
|
||||||
*
|
*
|
||||||
* @param record Dispatch record that needs fulfillment.
|
* @param record Dispatch record that needs fulfillment.
|
||||||
|
|
@ -294,8 +290,9 @@ public class PathFinderService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = gpsCalculationService.calculateDistance(
|
double distance =
|
||||||
servicePointLocation, record.delivery());
|
gpsCalculationService.calculateDistance(
|
||||||
|
servicePointLocation, record.delivery());
|
||||||
|
|
||||||
if (distance < bestScore) {
|
if (distance < bestScore) {
|
||||||
bestScore = distance;
|
bestScore = distance;
|
||||||
|
|
@ -309,16 +306,14 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Break a sequence of deliveries into several trips that each respect the drone
|
* Break a sequence of deliveries into several trips that each respect the drone move limit. The
|
||||||
* move limit. The
|
|
||||||
* deliveries should already be ordered by proximity for sensible grouping.
|
* deliveries should already be ordered by proximity for sensible grouping.
|
||||||
*
|
*
|
||||||
* @param deliveries Deliveries assigned to a drone.
|
* @param deliveries Deliveries assigned to a drone.
|
||||||
* @param drone Drone that will service the deliveries.
|
* @param drone Drone that will service the deliveries.
|
||||||
* @param servicePoint Starting and ending point of every trip.
|
* @param servicePoint Starting and ending point of every trip.
|
||||||
* @return Partitioned trips with at least one delivery each.
|
* @return Partitioned trips with at least one delivery each.
|
||||||
* @throws IllegalStateException If a single delivery exceeds the drone's move
|
* @throws IllegalStateException If a single delivery exceeds the drone's move limit.
|
||||||
* limit.
|
|
||||||
*/
|
*/
|
||||||
private List<List<MedDispatchRecRequest>> splitTrips(
|
private List<List<MedDispatchRecRequest>> splitTrips(
|
||||||
List<MedDispatchRecRequest> deliveries, Drone drone, LngLat servicePoint) {
|
List<MedDispatchRecRequest> deliveries, Drone drone, LngLat servicePoint) {
|
||||||
|
|
@ -354,15 +349,13 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a single trip for the provided drone, including the entire flight path
|
* Build a single trip for the provided drone, including the entire flight path to every
|
||||||
* to every
|
* delivery and back home. The resulting structure contains the {@link DronePath} representation
|
||||||
* delivery and back home. The resulting structure contains the
|
|
||||||
* {@link DronePath} representation
|
|
||||||
* as well as cost and moves consumed.
|
* as well as cost and moves consumed.
|
||||||
*
|
*
|
||||||
* @param drone Drone executing the trip.
|
* @param drone Drone executing the trip.
|
||||||
* @param servicePoint Starting/ending location of the trip.
|
* @param servicePoint Starting/ending location of the trip.
|
||||||
* @param deliveries Deliveries to include in the trip in execution order.
|
* @param deliveries Deliveries to include in the trip in execution order.
|
||||||
* @return Trip information or {@code null} if no deliveries are provided.
|
* @return Trip information or {@code null} if no deliveries are provided.
|
||||||
* @see DeliveryPathResponse.DronePath
|
* @see DeliveryPathResponse.DronePath
|
||||||
*/
|
*/
|
||||||
|
|
@ -402,9 +395,10 @@ public class PathFinderService {
|
||||||
flightPlans.add(new Delivery(delivery.id(), flightPath));
|
flightPlans.add(new Delivery(delivery.id(), flightPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
float cost = drone.capability().costInitial()
|
float cost =
|
||||||
+ drone.capability().costFinal()
|
drone.capability().costInitial()
|
||||||
+ (float) (drone.capability().costPerMove() * moves);
|
+ drone.capability().costFinal()
|
||||||
|
+ (float) (drone.capability().costPerMove() * moves);
|
||||||
|
|
||||||
DronePath path = new DronePath(drone.parseId(), flightPlans);
|
DronePath path = new DronePath(drone.parseId(), flightPlans);
|
||||||
|
|
||||||
|
|
@ -412,12 +406,11 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the number of moves a prospective trip would need by replaying the
|
* Estimate the number of moves a prospective trip would need by replaying the path calculation
|
||||||
* path calculation
|
|
||||||
* without mutating any persistent state.
|
* without mutating any persistent state.
|
||||||
*
|
*
|
||||||
* @param servicePoint Trip origin.
|
* @param servicePoint Trip origin.
|
||||||
* @param deliveries Deliveries that would compose the trip.
|
* @param deliveries Deliveries that would compose the trip.
|
||||||
* @return Total moves required to fly the proposed itinerary.
|
* @return Total moves required to fly the proposed itinerary.
|
||||||
*/
|
*/
|
||||||
private int estimateTripMoves(LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
private int estimateTripMoves(LngLat servicePoint, List<MedDispatchRecRequest> deliveries) {
|
||||||
|
|
@ -436,11 +429,10 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a path between {@code start} and {@code target} by repeatedly moving in
|
* Build a path between {@code start} and {@code target} by repeatedly moving in snapped
|
||||||
* snapped
|
|
||||||
* increments while avoiding restricted zones.
|
* increments while avoiding restricted zones.
|
||||||
*
|
*
|
||||||
* @param start Start coordinate.
|
* @param start Start coordinate.
|
||||||
* @param target Destination coordinate.
|
* @param target Destination coordinate.
|
||||||
* @return Sequence of visited coordinates and move count.
|
* @return Sequence of visited coordinates and move count.
|
||||||
* @see #nextPosition(LngLat, LngLat)
|
* @see #nextPosition(LngLat, LngLat)
|
||||||
|
|
@ -470,20 +462,18 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the next position on the path from {@code current} toward
|
* Determine the next position on the path from {@code current} toward {@code target},
|
||||||
* {@code target},
|
* preferring the snapped angle closest to the desired heading that does not infiltrate a
|
||||||
* preferring the snapped angle closest to the desired heading that does not
|
|
||||||
* infiltrate a
|
|
||||||
* restricted region.
|
* restricted region.
|
||||||
*
|
*
|
||||||
* @param current Current coordinate.
|
* @param current Current coordinate.
|
||||||
* @param target Destination coordinate.
|
* @param target Destination coordinate.
|
||||||
* @return Next admissible coordinate or the original point if none can be
|
* @return Next admissible coordinate or the original point if none can be found.
|
||||||
* found.
|
|
||||||
*/
|
*/
|
||||||
private LngLat nextPosition(LngLat current, LngLat target) {
|
private LngLat nextPosition(LngLat current, LngLat target) {
|
||||||
double desiredAngle = Math.toDegrees(
|
double desiredAngle =
|
||||||
Math.atan2(target.lat() - current.lat(), target.lng() - current.lng()));
|
Math.toDegrees(
|
||||||
|
Math.atan2(target.lat() - current.lat(), target.lng() - current.lng()));
|
||||||
List<Angle> candidateAngles = buildAngleCandidates(desiredAngle);
|
List<Angle> candidateAngles = buildAngleCandidates(desiredAngle);
|
||||||
for (Angle angle : candidateAngles) {
|
for (Angle angle : candidateAngles) {
|
||||||
LngLat next = gpsCalculationService.nextPosition(current, angle);
|
LngLat next = gpsCalculationService.nextPosition(current, angle);
|
||||||
|
|
@ -495,10 +485,8 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a sequence of candidate angles centered on the desired heading,
|
* Build a sequence of candidate angles centered on the desired heading, expanding symmetrically
|
||||||
* expanding symmetrically
|
* clockwise and counter-clockwise to explore alternative headings if the primary path is
|
||||||
* clockwise and counter-clockwise to explore alternative headings if the
|
|
||||||
* primary path is
|
|
||||||
* blocked.
|
* blocked.
|
||||||
*
|
*
|
||||||
* @param desiredAngle Bearing in degrees between current and target positions.
|
* @param desiredAngle Bearing in degrees between current and target positions.
|
||||||
|
|
@ -533,17 +521,15 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a computed path segment wrapping the visited positions and
|
* Representation of a computed path segment wrapping the visited positions and the number of
|
||||||
* the number of
|
|
||||||
* moves taken to traverse them.
|
* moves taken to traverse them.
|
||||||
*
|
*
|
||||||
* @param positions Ordered coordinates that describe the path.
|
* @param positions Ordered coordinates that describe the path.
|
||||||
* @param moves Number of moves consumed by the path.
|
* @param moves Number of moves consumed by the path.
|
||||||
*/
|
*/
|
||||||
private record PathSegment(List<LngLat> positions, int moves) {
|
private record PathSegment(List<LngLat> positions, int moves) {
|
||||||
/**
|
/**
|
||||||
* Append the positions from this segment to {@code target}, skipping the first
|
* Append the positions from this segment to {@code target}, skipping the first coordinate
|
||||||
* coordinate
|
|
||||||
* as it is already represented by the last coordinate in the consumer path.
|
* as it is already represented by the last coordinate in the consumer path.
|
||||||
*
|
*
|
||||||
* @param target Mutable list to append to.
|
* @param target Mutable list to append to.
|
||||||
|
|
@ -556,10 +542,8 @@ public class PathFinderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundle containing the calculated {@link DronePath}, total moves and financial
|
* Bundle containing the calculated {@link DronePath}, total moves and financial cost for a
|
||||||
* cost for a
|
|
||||||
* single trip.
|
* single trip.
|
||||||
*/
|
*/
|
||||||
private record TripResult(DronePath path, int moves, float cost) {
|
private record TripResult(DronePath path, int moves, float cost) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
package io.github.js0ny.ilp_coursework.service;
|
package io.github.js0ny.ilp_coursework.service;
|
||||||
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.github.js0ny.ilp_coursework.data.common.DroneEvent;
|
import io.github.js0ny.ilp_coursework.data.common.DroneEvent;
|
||||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TelemetryService {
|
public class TelemetryService {
|
||||||
private final HttpClient client;
|
private final HttpClient client;
|
||||||
|
|
@ -24,7 +24,8 @@ public class TelemetryService {
|
||||||
this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
|
||||||
|
|
||||||
this.mapper = new ObjectMapper();
|
this.mapper = new ObjectMapper();
|
||||||
this.BLACKBOX_URL = System.getenv().getOrDefault("BLACKBOX_ENDPOINT", "http://localhost:3000");
|
this.BLACKBOX_URL =
|
||||||
|
System.getenv().getOrDefault("BLACKBOX_ENDPOINT", "http://localhost:3000");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp) {
|
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp) {
|
||||||
|
|
@ -34,15 +35,16 @@ public class TelemetryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp, LocalDateTime baseTimestamp) {
|
public void sendEventAsyncByPathResponse(
|
||||||
|
DeliveryPathResponse resp, LocalDateTime baseTimestamp) {
|
||||||
var events = DroneEvent.fromPathResponseWithTimestamp(resp, baseTimestamp);
|
var events = DroneEvent.fromPathResponseWithTimestamp(resp, baseTimestamp);
|
||||||
for (var event : events) {
|
for (var event : events) {
|
||||||
sendEventAsync(event);
|
sendEventAsync(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp,
|
public void sendEventAsyncByPathResponse(
|
||||||
Map<Integer, LocalDateTime> deliveryTimestamps) {
|
DeliveryPathResponse resp, Map<Integer, LocalDateTime> deliveryTimestamps) {
|
||||||
var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps);
|
var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps);
|
||||||
for (var event : events) {
|
for (var event : events) {
|
||||||
sendEventAsync(event);
|
sendEventAsync(event);
|
||||||
|
|
@ -50,22 +52,25 @@ public class TelemetryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendEventAsync(DroneEvent event) {
|
public void sendEventAsync(DroneEvent event) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(
|
||||||
try {
|
() -> {
|
||||||
String json = mapper.writeValueAsString(event);
|
try {
|
||||||
System.out.println("[INFO] Sending telemetry event: " + json);
|
String json = mapper.writeValueAsString(event);
|
||||||
var request = java.net.http.HttpRequest.newBuilder()
|
System.out.println("[INFO] Sending telemetry event: " + json);
|
||||||
.uri(java.net.URI.create(BLACKBOX_URL + "/ingest"))
|
var request =
|
||||||
.header("Content-Type", "application/json")
|
java.net.http.HttpRequest.newBuilder()
|
||||||
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(json))
|
.uri(java.net.URI.create(BLACKBOX_URL + "/ingest"))
|
||||||
.build();
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(
|
||||||
client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
java.net.http.HttpRequest.BodyPublishers.ofString(
|
||||||
} catch (Exception e) {
|
json))
|
||||||
System.err.println("[ERROR] Failed to send telemetry event: " + e.getMessage());
|
.build();
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println(
|
||||||
|
"[ERROR] Failed to send telemetry event: " + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue