diff --git a/.gitignore b/.gitignore index e8e3c27..651fdcf 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ drone-black-box/drone-black-box drone-black-box/drone_black_box.db* *.pdf + +*.out +*.html diff --git a/.justfile b/.justfile new file mode 100644 index 0000000..49c0e6a --- /dev/null +++ b/.justfile @@ -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 diff --git a/drone-black-box/main_test.go b/drone-black-box/main_test.go index 6fc24b0..b6537e9 100644 --- a/drone-black-box/main_test.go +++ b/drone-black-box/main_test.go @@ -125,6 +125,7 @@ func TestIngestBadJSON(t *testing.T) { // Ensure graceful shutdown path does not hang: start a server and shut it down quickly. func TestGracefulShutdown(t *testing.T) { db := newTestDB(t) + defer db.Close() srv := &Server{db: db} mux := http.NewServeMux() mux.HandleFunc("GET /health", srv.healthHandler) @@ -138,3 +139,75 @@ func TestGracefulShutdown(t *testing.T) { 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) + } +} diff --git a/ilp-rest-service/build.gradle b/ilp-rest-service/build.gradle index 81be9e6..94a836e 100644 --- a/ilp-rest-service/build.gradle +++ b/ilp-rest-service/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.6' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' } group = 'io.github.js0ny' @@ -26,6 +27,56 @@ dependencies { } +jacoco { + toolVersion = "0.8.12" +} + tasks.named('test') { 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 diff --git a/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru b/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru index 072c15f..d31d50a 100644 --- a/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru +++ b/ilp-rest-service/ilp-cw-api/Front-end Test/2 Drones.bru @@ -1,5 +1,5 @@ meta { - name: 2 Drones + name: 2 Drones type: http seq: 9 } @@ -14,7 +14,7 @@ body:json { [ { "id": 123, - "date": "2025-12-22", + "date": "2025-12-25", "time": "14:30", "requirements": { "capacity": 0.75, @@ -28,7 +28,7 @@ body:json { }, { "id": 456, - "date": "2025-12-25", + "date": "2025-12-23", "time": "11:30", "requirements": { "capacity": 0.75, diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java index d8511e5..63a949d 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/config/CorsConfig.java @@ -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.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 public class CorsConfig implements WebMvcConfigurer { - private static final String[] ALLOWED_ORIGINS = new String[] { - "http://localhost:4173", - "http://127.0.0.1:4173", - "http://localhost:5173", - "http://127.0.0.1:5173" - }; + private static final String[] ALLOWED_ORIGINS = + new String[] { + "http://localhost:4173", + "http://127.0.0.1:4173", + "http://localhost:5173", + "http://127.0.0.1:5173" + }; @Override public void addCorsMappings(CorsRegistry registry) { diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java index d4c81b9..8c38e47 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/controller/MapMetaController.java @@ -1,14 +1,14 @@ 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; -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 java.util.List; @RestController @RequestMapping("/api/v1") diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java index a056863..79ca2f6 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java @@ -1,11 +1,11 @@ package io.github.js0ny.ilp_coursework.data.common; +import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; + import java.time.LocalDateTime; import java.util.List; import java.util.Map; -import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; - // Corresponding in Go // @@ -18,13 +18,10 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; * } */ -public record DroneEvent( - String droneId, - double latitude, - double longitude, - String timestamp) { +public record DroneEvent(String droneId, double latitude, double longitude, String timestamp) { + + static final int STEP = 1; // seconds between events - final static int STEP = 1; // seconds between events // Helper method that converts from DeliveryPathResponse to List public static List fromPathResponse(DeliveryPathResponse resp) { @@ -34,11 +31,7 @@ public record DroneEvent( for (var d : p.deliveries()) { for (var coord : d.flightPath()) { String timestamp = java.time.Instant.now().toString(); - events.add(new DroneEvent( - id, - coord.lat(), - coord.lng(), - timestamp)); + events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp)); } } } @@ -47,19 +40,15 @@ public record DroneEvent( // Helper method that converts from DeliveryPathResponse to List // with base timestamp - public static List fromPathResponseWithTimestamp(DeliveryPathResponse resp, - LocalDateTime baseTimestamp) { + public static List fromPathResponseWithTimestamp( + DeliveryPathResponse resp, LocalDateTime baseTimestamp) { List events = new java.util.ArrayList<>(); java.time.LocalDateTime timestamp = baseTimestamp; for (var p : resp.dronePaths()) { String id = String.valueOf(p.droneId()); for (var d : p.deliveries()) { for (var coord : d.flightPath()) { - events.add(new DroneEvent( - id, - coord.lat(), - coord.lng(), - timestamp.toString())); + events.add(new DroneEvent(id, coord.lat(), coord.lng(), timestamp.toString())); timestamp = timestamp.plusSeconds(STEP); // Increment timestamp for each event } } @@ -75,14 +64,11 @@ public record DroneEvent( for (var d : p.deliveries()) { LocalDateTime timestamp = deliveryTimestamps.get(d.deliveryId()); // 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(); for (var coord : d.flightPath()) { - events.add(new DroneEvent( - id, - coord.lat(), - coord.lng(), - current.toString())); + events.add(new DroneEvent(id, coord.lat(), coord.lng(), current.toString())); current = current.plusSeconds(STEP); } } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java index 35ed1e0..44719f2 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/DroneInfoService.java @@ -1,10 +1,7 @@ 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.Region; 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; @@ -54,15 +51,13 @@ public class DroneInfoService { /** * Return an array of ids of drones with/without cooling capability * - *

- * Associated service method with {@code /dronesWithCooling/{state}} + *

Associated service method with {@code /dronesWithCooling/{state}} * * @param state determines the capability filtering - * @return if {@code state} is true, return ids of drones with cooling - * capability, else without - * cooling + * @return if {@code state} is true, return ids of drones with cooling capability, else without + * cooling * @see - * io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) + * io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean) */ public List dronesWithCooling(boolean state) { // 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} * - *

- * Associated service method with {@code /droneDetails/{id}} + *

Associated service method with {@code /droneDetails/{id}} * * @param id The id of the drone * @return drone json body of given id - * @throws NullPointerException when cannot fetch available drones from - * remote - * @throws IllegalArgumentException when drone with given {@code id} cannot be - * found this should - * lead to a 404 + * @throws NullPointerException when cannot fetch available drones from remote + * @throws IllegalArgumentException when drone with given {@code id} cannot be found this should + * lead to a 404 * @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String) */ 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 - * medical dispatch + * Return an array of ids of drones that match all the requirements in the medical dispatch * records * - *

- * Associated service method with + *

Associated service method with * * @param rec array of medical dispatch records * @return List of drone ids that match all the requirements @@ -135,9 +125,10 @@ public class DroneInfoService { return drones.stream() .filter(d -> d != null && d.capability() != null) .filter( - d -> Arrays.stream(rec) - .filter(r -> r != null && r.requirements() != null) - .allMatch(r -> droneMatchesRequirement(d, r))) + d -> + Arrays.stream(rec) + .filter(r -> r != null && r.requirements() != null) + .allMatch(r -> droneMatchesRequirement(d, r))) .map(Drone::id) .collect(Collectors.toList()); } @@ -145,13 +136,11 @@ public class DroneInfoService { /** * 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 * @return true if the drone meets the requirement, false otherwise - * @throws IllegalArgumentException when record requirements or drone capability - * is invalid - * (capacity and id cannot be null in - * {@code MedDispathRecDto}) + * @throws IllegalArgumentException when record requirements or drone capability is invalid + * (capacity and id cannot be null in {@code MedDispathRecDto}) */ public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) { 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 * * @param droneId the id of the drone to be checked - * @param record the medical dispatch record containing the required date and - * time + * @param record the medical dispatch record containing the required date and time * @return true if the drone is available, false otherwise */ private boolean checkAvailability(String droneId, MedDispatchRecRequest record) { 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(); DayOfWeek requiredDay = requiredDate.getDayOfWeek(); @@ -216,7 +205,8 @@ public class DroneInfoService { private LngLat queryServicePointLocationByDroneId(String droneId) { URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); - ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class); + ServicePointDrones[] servicePoints = + restTemplate.getForObject(droneUrl, ServicePointDrones[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -233,7 +223,8 @@ public class DroneInfoService { private LngLat queryServicePointLocation(int id) { URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); - ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class); + ServicePoint[] servicePoints = + restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; for (var sp : servicePoints) { @@ -256,22 +247,24 @@ public class DroneInfoService { public List fetchRestrictedAreas() { URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint); - RestrictedArea[] restrictedAreas = restTemplate.getForObject(restrictedUrl, RestrictedArea[].class); + RestrictedArea[] restrictedAreas = + restTemplate.getForObject(restrictedUrl, RestrictedArea[].class); assert restrictedAreas != null; return Arrays.asList(restrictedAreas); } public List fetchServicePoints() { URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint); - ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class); + ServicePoint[] servicePoints = + restTemplate.getForObject(servicePointUrl, ServicePoint[].class); assert servicePoints != null; return Arrays.asList(servicePoints); } public List fetchDronesForServicePoints() { URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint); - ServicePointDrones[] servicePointDrones = restTemplate.getForObject(servicePointDronesUrl, - ServicePointDrones[].class); + ServicePointDrones[] servicePointDrones = + restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class); assert servicePointDrones != null; return Arrays.asList(servicePointDrones); } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java index ab6ef15..b9d1bbc 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/PathFinderService.java @@ -23,6 +23,7 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePa import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -31,7 +32,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import java.time.LocalDateTime; /** * Class that handles calculations about deliverypath @@ -44,10 +44,8 @@ import java.time.LocalDateTime; public class PathFinderService { /** - * Hard stop on how many pathfinding iterations we attempt for a single segment - * before bailing, - * useful for preventing infinite loops caused by precision quirks or unexpected - * map data. + * Hard stop on how many pathfinding iterations we attempt for a single segment before bailing, + * useful for preventing infinite loops caused by precision quirks or unexpected map data. * * @see #computePath(LngLat, LngLat) */ @@ -67,14 +65,11 @@ public class PathFinderService { private TelemetryService telemetryService; /** - * Constructor for PathFinderService. The dependencies are injected by Spring - * and the - * constructor pre-computes reference maps used throughout the request - * lifecycle. + * Constructor for PathFinderService. The dependencies are injected by Spring and the + * constructor pre-computes reference maps used throughout the request lifecycle. * * @param gpsCalculationService Service handling geometric operations. - * @param droneInfoService Service that exposes drone metadata and - * capability information. + * @param droneInfoService Service that exposes drone metadata and capability information. */ public PathFinderService( GpsCalculationService gpsCalculationService, DroneInfoService droneInfoService) { @@ -87,7 +82,8 @@ public class PathFinderService { this.drones = droneInfoService.fetchAllDrones(); List servicePoints = droneInfoService.fetchServicePoints(); - List servicePointAssignments = droneInfoService.fetchDronesForServicePoints(); + List servicePointAssignments = + droneInfoService.fetchDronesForServicePoints(); List restrictedAreas = droneInfoService.fetchRestrictedAreas(); this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone)); @@ -105,17 +101,17 @@ public class PathFinderService { } } - this.servicePointLocations = servicePoints.stream() - .collect( - Collectors.toMap( - ServicePoint::id, sp -> new LngLat(sp.location()))); + this.servicePointLocations = + servicePoints.stream() + .collect( + Collectors.toMap( + ServicePoint::id, sp -> new LngLat(sp.location()))); this.restrictedRegions = restrictedAreas.stream().map(RestrictedArea::toRegion).toList(); } /** - * Produce a delivery plan for the provided dispatch records. Deliveries are - * grouped per + * Produce a delivery plan for the provided dispatch records. Deliveries are grouped per * compatible drone and per trip to satisfy each drone move limit. * * @param records Dispatch records to be fulfilled. @@ -161,14 +157,17 @@ public class PathFinderService { continue; } - List sortedDeliveries = entry.getValue().stream() - .sorted( - Comparator.comparingDouble( - rec -> gpsCalculationService.calculateDistance( - servicePointLocation, rec.delivery()))) - .toList(); + List sortedDeliveries = + entry.getValue().stream() + .sorted( + Comparator.comparingDouble( + rec -> + gpsCalculationService.calculateDistance( + servicePointLocation, rec.delivery()))) + .toList(); - List> trips = splitTrips(sortedDeliveries, drone, servicePointLocation); + List> trips = + splitTrips(sortedDeliveries, drone, servicePointLocation); for (List trip : trips) { TripResult result = buildTrip(drone, servicePointLocation, trip); @@ -195,9 +194,9 @@ public class PathFinderService { * GeoJSON FeatureCollection suitable for mapping visualization. * * @param records Dispatch records to be fulfilled. - * + * * @return GeoJSON payload representing every delivery flight path. - * + * * @throws IllegalStateException When the payload cannot be serialized. */ public String calculateDeliveryPathAsGeoJson(MedDispatchRecRequest[] records) { @@ -246,10 +245,8 @@ public class PathFinderService { } /** - * Group dispatch records by their assigned drone, ensuring every record is - * routed through - * {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding - * invalid entries. + * Group dispatch records by their assigned drone, ensuring every record is routed through + * {@link #findBestDrone(MedDispatchRecRequest)} exactly once and discarding invalid entries. * * @param records Dispatch records to be grouped. * @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 - * picking the closest + * Choose the best drone for the provided record. Currently that equates to picking the closest * compatible drone to the delivery location. * * @param record Dispatch record that needs fulfillment. @@ -294,8 +290,9 @@ public class PathFinderService { continue; } - double distance = gpsCalculationService.calculateDistance( - servicePointLocation, record.delivery()); + double distance = + gpsCalculationService.calculateDistance( + servicePointLocation, record.delivery()); if (distance < bestScore) { bestScore = distance; @@ -309,16 +306,14 @@ public class PathFinderService { } /** - * Break a sequence of deliveries into several trips that each respect the drone - * move limit. The + * Break a sequence of deliveries into several trips that each respect the drone move limit. The * deliveries should already be ordered by proximity for sensible grouping. * - * @param deliveries Deliveries assigned to a drone. - * @param drone Drone that will service the deliveries. + * @param deliveries Deliveries assigned to a drone. + * @param drone Drone that will service the deliveries. * @param servicePoint Starting and ending point of every trip. * @return Partitioned trips with at least one delivery each. - * @throws IllegalStateException If a single delivery exceeds the drone's move - * limit. + * @throws IllegalStateException If a single delivery exceeds the drone's move limit. */ private List> splitTrips( List 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 - * to every - * delivery and back home. The resulting structure contains the - * {@link DronePath} representation + * Build a single trip for the provided drone, including the entire flight path to every + * delivery and back home. The resulting structure contains the {@link DronePath} representation * 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 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. * @see DeliveryPathResponse.DronePath */ @@ -402,9 +395,10 @@ public class PathFinderService { flightPlans.add(new Delivery(delivery.id(), flightPath)); } - float cost = drone.capability().costInitial() - + drone.capability().costFinal() - + (float) (drone.capability().costPerMove() * moves); + float cost = + drone.capability().costInitial() + + drone.capability().costFinal() + + (float) (drone.capability().costPerMove() * moves); 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 - * path calculation + * Estimate the number of moves a prospective trip would need by replaying the path calculation * without mutating any persistent state. * * @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. */ private int estimateTripMoves(LngLat servicePoint, List deliveries) { @@ -436,11 +429,10 @@ public class PathFinderService { } /** - * Build a path between {@code start} and {@code target} by repeatedly moving in - * snapped + * Build a path between {@code start} and {@code target} by repeatedly moving in snapped * increments while avoiding restricted zones. * - * @param start Start coordinate. + * @param start Start coordinate. * @param target Destination coordinate. * @return Sequence of visited coordinates and move count. * @see #nextPosition(LngLat, LngLat) @@ -470,20 +462,18 @@ public class PathFinderService { } /** - * Determine the next position on the path from {@code current} toward - * {@code target}, - * preferring the snapped angle closest to the desired heading that does not - * infiltrate a + * Determine the next position on the path from {@code current} toward {@code target}, + * preferring the snapped angle closest to the desired heading that does not infiltrate a * restricted region. * * @param current Current coordinate. - * @param target Destination coordinate. - * @return Next admissible coordinate or the original point if none can be - * found. + * @param target Destination coordinate. + * @return Next admissible coordinate or the original point if none can be found. */ private LngLat nextPosition(LngLat current, LngLat target) { - double desiredAngle = Math.toDegrees( - Math.atan2(target.lat() - current.lat(), target.lng() - current.lng())); + double desiredAngle = + Math.toDegrees( + Math.atan2(target.lat() - current.lat(), target.lng() - current.lng())); List candidateAngles = buildAngleCandidates(desiredAngle); for (Angle angle : candidateAngles) { LngLat next = gpsCalculationService.nextPosition(current, angle); @@ -495,10 +485,8 @@ public class PathFinderService { } /** - * Build a sequence of candidate angles centered on the desired heading, - * expanding symmetrically - * clockwise and counter-clockwise to explore alternative headings if the - * primary path is + * Build a sequence of candidate angles centered on the desired heading, expanding symmetrically + * clockwise and counter-clockwise to explore alternative headings if the primary path is * blocked. * * @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 - * the number of + * Representation of a computed path segment wrapping the visited positions and the number of * moves taken to traverse them. * * @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 positions, int moves) { /** - * Append the positions from this segment to {@code target}, skipping the first - * coordinate + * Append the positions from this segment to {@code target}, skipping the first coordinate * as it is already represented by the last coordinate in the consumer path. * * @param target Mutable list to append to. @@ -556,10 +542,8 @@ public class PathFinderService { } /** - * Bundle containing the calculated {@link DronePath}, total moves and financial - * cost for a + * Bundle containing the calculated {@link DronePath}, total moves and financial cost for a * single trip. */ - private record TripResult(DronePath path, int moves, float cost) { - } + private record TripResult(DronePath path, int moves, float cost) {} } diff --git a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java index 33cfc24..9d95f0d 100644 --- a/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java @@ -1,18 +1,18 @@ 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 io.github.js0ny.ilp_coursework.data.common.DroneEvent; 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 public class TelemetryService { private final HttpClient client; @@ -24,7 +24,8 @@ public class TelemetryService { this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build(); 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) { @@ -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); for (var event : events) { sendEventAsync(event); } } - public void sendEventAsyncByPathResponse(DeliveryPathResponse resp, - Map deliveryTimestamps) { + public void sendEventAsyncByPathResponse( + DeliveryPathResponse resp, Map deliveryTimestamps) { var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps); for (var event : events) { sendEventAsync(event); @@ -50,22 +52,25 @@ public class TelemetryService { } public void sendEventAsync(DroneEvent event) { - CompletableFuture.runAsync(() -> { - try { - String json = mapper.writeValueAsString(event); - System.out.println("[INFO] Sending telemetry event: " + json); - var request = java.net.http.HttpRequest.newBuilder() - .uri(java.net.URI.create(BLACKBOX_URL + "/ingest")) - .header("Content-Type", "application/json") - .POST(java.net.http.HttpRequest.BodyPublishers.ofString(json)) - .build(); - - client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); - } catch (Exception e) { - System.err.println("[ERROR] Failed to send telemetry event: " + e.getMessage()); - } - }); + CompletableFuture.runAsync( + () -> { + try { + String json = mapper.writeValueAsString(event); + System.out.println("[INFO] Sending telemetry event: " + json); + var request = + java.net.http.HttpRequest.newBuilder() + .uri(java.net.URI.create(BLACKBOX_URL + "/ingest")) + .header("Content-Type", "application/json") + .POST( + java.net.http.HttpRequest.BodyPublishers.ofString( + json)) + .build(); + client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + System.err.println( + "[ERROR] Failed to send telemetry event: " + e.getMessage()); + } + }); } - }