From 8e462fedc1821fbc7446ed399c30680048387664 Mon Sep 17 00:00:00 2001 From: js0ny Date: Fri, 5 Dec 2025 06:40:58 +0000 Subject: [PATCH] glue go and java --- drone-black-box/main.go | 4 +- .../data/common/DroneEvent.java | 65 ++++++++ .../service/PathFinderService.java | 151 +++++++++++------- .../service/TelemetryService.java | 61 +++++++ 4 files changed, 217 insertions(+), 64 deletions(-) create mode 100644 ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java create mode 100644 ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java diff --git a/drone-black-box/main.go b/drone-black-box/main.go index 40dd7c5..76cca90 100644 --- a/drone-black-box/main.go +++ b/drone-black-box/main.go @@ -14,9 +14,9 @@ import ( _ "github.com/mattn/go-sqlite3" ) -// Define the DroneEvent struct as JSON +// DroneEvent defines the DroneEvent struct as JSON type DroneEvent struct { - DroneID string `json:"drone_id"` + DroneID string `json:"droneId"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Timestamp string `json:"timestamp"` 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 new file mode 100644 index 0000000..9132afe --- /dev/null +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/data/common/DroneEvent.java @@ -0,0 +1,65 @@ +package io.github.js0ny.ilp_coursework.data.common; + +import java.time.LocalDateTime; +import java.util.List; + +import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; + +// Corresponding in Go +// + +/* + * type DroneEvent struct { + * DroneID string `json:"drone_id"` + * Latitude float64 `json:"latitude"` + * Longitude float64 `json:"longitude"` + * Timestamp string `json:"timestamp"` + * } + */ + +public record DroneEvent( + String droneId, + double latitude, + double longitude, + String timestamp) { + // Helper method that converts from DeliveryPathResponse to List + public static List fromPathResponse(DeliveryPathResponse resp) { + List events = new java.util.ArrayList<>(); + for (var p : resp.dronePaths()) { + String id = p.droneId() + ""; + 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)); + } + } + } + return events; + } + + // Helper method that converts from DeliveryPathResponse to List + // with base timestamp + 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())); + timestamp = timestamp.plusSeconds(1); // Increment timestamp for each event + } + } + } + return events; + } +} 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 6195f4f..44b52b8 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 @@ -20,6 +20,7 @@ import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath; import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse.DronePath.Delivery; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -42,8 +43,10 @@ import java.util.stream.Collectors; 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) */ @@ -59,12 +62,18 @@ public class PathFinderService { private final Map servicePointLocations; private final List restrictedRegions; + @Autowired + 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) { @@ -77,8 +86,7 @@ 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)); @@ -96,17 +104,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. @@ -148,17 +156,14 @@ 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); @@ -170,15 +175,22 @@ public class PathFinderService { } } - return new DeliveryPathResponse(totalCost, totalMoves, paths.toArray(new DronePath[0])); + var resp = new DeliveryPathResponse(totalCost, totalMoves, paths.toArray(new DronePath[0])); + + telemetryService.sendEventAsyncByPathResponse(resp); + + return resp; } - /** - * Convenience wrapper around {@link #calculateDeliveryPath} that serializes the result into a + /* + * Convenience wrapper around {@link #calculateDeliveryPath} that serializes the + * result into a * 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) { @@ -227,8 +239,10 @@ 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. @@ -247,7 +261,8 @@ 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. @@ -272,9 +287,8 @@ public class PathFinderService { continue; } - double distance = - gpsCalculationService.calculateDistance( - servicePointLocation, record.delivery()); + double distance = gpsCalculationService.calculateDistance( + servicePointLocation, record.delivery()); if (distance < bestScore) { bestScore = distance; @@ -288,14 +302,16 @@ 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) { @@ -331,13 +347,15 @@ 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 */ @@ -377,10 +395,9 @@ 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); @@ -388,11 +405,12 @@ 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) { @@ -411,10 +429,11 @@ 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) @@ -444,18 +463,20 @@ 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); @@ -467,8 +488,10 @@ 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. @@ -503,15 +526,17 @@ 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. @@ -524,8 +549,10 @@ 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 new file mode 100644 index 0000000..9d2e19e --- /dev/null +++ b/ilp-rest-service/src/main/java/io/github/js0ny/ilp_coursework/service/TelemetryService.java @@ -0,0 +1,61 @@ +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 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; + +@Service +public class TelemetryService { + private final HttpClient client; + private final ObjectMapper mapper; + + private final String BLACKBOX_URL; + + public 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"); + } + + public void sendEventAsyncByPathResponse(DeliveryPathResponse resp) { + var events = DroneEvent.fromPathResponse(resp); + for (var event : events) { + sendEventAsync(event); + } + } + + public void sendEventAsyncByPathResponse(DeliveryPathResponse resp, LocalDateTime baseTimestamp) { + var events = DroneEvent.fromPathResponseWithTimestamp(resp, baseTimestamp); + for (var event : events) { + sendEventAsync(event); + } + } + + public void sendEventAsync(DroneEvent event) { + CompletableFuture.runAsync(() -> { + try { + String json = mapper.writeValueAsString(event); + 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()); + } + }); + + } + +}