Compare commits
No commits in common. "f013955bc2358451a5202300b908868aaf31e726" and "15ad7a2fb736e23dfb1e71b32d1629a39eb5bb5c" have entirely different histories.
f013955bc2
...
15ad7a2fb7
21 changed files with 209 additions and 882 deletions
54
.bin/e2e.sh
54
.bin/e2e.sh
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
|
||||
|
||||
echo "[INFO] Starting Go Backend..."
|
||||
cd drone-black-box && go build -o app main.go
|
||||
cd ..
|
||||
|
||||
./drone-black-box/app &
|
||||
GO_PID=$!
|
||||
echo " Go PID: $GO_PID"
|
||||
|
||||
echo "[INFO] Starting Java Backend..."
|
||||
|
||||
cd ilp-rest-service
|
||||
./gradlew bootRun &
|
||||
JAVA_PID=$!
|
||||
echo " Java PID: $JAVA_PID"
|
||||
cd ..
|
||||
|
||||
cleanup() {
|
||||
echo "[INFO] Stopping services..."
|
||||
kill $GO_PID || true
|
||||
kill $JAVA_PID || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
||||
echo "[INFO] Waiting for services to be ready..."
|
||||
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:8080/actuator/health > /dev/null; then
|
||||
echo "[INFO] Java is UP!"
|
||||
break
|
||||
fi
|
||||
echo "[DEBUG] Waiting for Java..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
|
||||
echo "[INFO] Running Bruno E2E Collection..."
|
||||
|
||||
cd ./ilp-rest-service/ilp-cw-api
|
||||
|
||||
bru run
|
||||
|
||||
echo "[INFO] E2E Tests Passed!"
|
||||
|
||||
rm --force drone_black_box.db
|
||||
rm --force drone_black_box.db-wal
|
||||
rm --force drone_black_box.db-shm
|
||||
|
||||
cleanup
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
|
|
@ -1,32 +0,0 @@
|
|||
name: Polyglot CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.go'
|
||||
- '.justfile'
|
||||
- '**.js'
|
||||
- '**.svelte'
|
||||
- '**.css'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v25
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v14
|
||||
with:
|
||||
name: mycache
|
||||
- run: chmod +x ./ilp-rest-service/gradlew
|
||||
- name: Unit Testing and Integration Testing
|
||||
run: nix develop .#ci --command just all
|
||||
- name: E2E Test
|
||||
run: nix develop .#ci --command just e2e
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -47,9 +47,3 @@ drone-black-box/drone-black-box
|
|||
drone-black-box/drone_black_box.db*
|
||||
|
||||
*.pdf
|
||||
|
||||
*.out
|
||||
*.html
|
||||
|
||||
drone_black_box.db*
|
||||
app
|
||||
|
|
|
|||
29
.justfile
29
.justfile
|
|
@ -1,29 +0,0 @@
|
|||
all:
|
||||
@just format
|
||||
@just static-analysis
|
||||
@just test
|
||||
|
||||
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
|
||||
|
||||
e2e:
|
||||
bash .bin/e2e.sh
|
||||
|
||||
benchmark:
|
||||
echo "Make sure the server is running at http://localhost:3000"
|
||||
oha -z 10s -c 50 -m POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"drone_id": "TEST-DRONE", "latitude": 0.0, "longitude": 0.0, "timestamp": "2025-01-01"}' \
|
||||
http://localhost:3000/ingest
|
||||
|
|
@ -1,9 +1,5 @@
|
|||
# Informatics Large Practical Coursework 3
|
||||
|
||||
This is a temporary repository, if you are viewing on GitHub, just ignore the (randomly generated) repository name.
|
||||
|
||||
[](https://github.com/js0ny/expert-goggles/actions/workflows/ci.yml)
|
||||
|
||||
## Installation
|
||||
|
||||
### Docker Compose
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ 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)
|
||||
|
|
@ -139,75 +138,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
flake.nix
55
flake.nix
|
|
@ -17,42 +17,29 @@
|
|||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
ciDeps = with pkgs; [
|
||||
jdk21
|
||||
gradle
|
||||
google-java-format
|
||||
go
|
||||
just
|
||||
fd
|
||||
bruno-cli
|
||||
];
|
||||
devDeps = with pkgs; [
|
||||
vscode-langservers-extracted
|
||||
jdt-language-server
|
||||
jless
|
||||
httpie
|
||||
docker
|
||||
docker-compose
|
||||
newman
|
||||
gron
|
||||
fx
|
||||
oha
|
||||
gopls
|
||||
bun
|
||||
svelte-language-server
|
||||
typescript-language-server
|
||||
prettier
|
||||
];
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = ciDeps ++ devDeps;
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}
|
||||
echo "Java: $(java --version | head -n 1)"
|
||||
'';
|
||||
};
|
||||
ci = pkgs.mkShell {
|
||||
buildInputs = ciDeps;
|
||||
buildInputs = with pkgs; [
|
||||
vscode-langservers-extracted
|
||||
jdt-language-server
|
||||
jless
|
||||
jdk21
|
||||
gradle
|
||||
httpie
|
||||
docker
|
||||
docker-compose
|
||||
newman
|
||||
gron
|
||||
fx
|
||||
google-java-format
|
||||
oha
|
||||
gopls
|
||||
go
|
||||
bun
|
||||
svelte-language-server
|
||||
typescript-language-server
|
||||
prettier
|
||||
];
|
||||
shellHook = ''
|
||||
export JAVA_HOME=${pkgs.jdk21}
|
||||
echo "Java: $(java --version | head -n 1)"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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'
|
||||
|
|
@ -27,58 +26,6 @@ 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/*',
|
||||
'**/TelemetryService.class'
|
||||
])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
element = 'CLASS'
|
||||
|
||||
excludes = [
|
||||
'io.github.js0ny.ilp_coursework.IlpCourseworkApplication',
|
||||
'**.config.**',
|
||||
'**.data.**',
|
||||
'**.util.**',
|
||||
'io.github.js0ny.ilp_coursework.service.TelemetryService'
|
||||
]
|
||||
|
||||
limit {
|
||||
counter = 'BRANCH'
|
||||
value = 'COVEREDRATIO'
|
||||
|
||||
minimum = 0.50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
check.dependsOn jacocoTestCoverageVerification
|
||||
|
|
|
|||
|
|
@ -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-25",
|
||||
"date": "2025-12-22",
|
||||
"time": "14:30",
|
||||
"requirements": {
|
||||
"capacity": 0.75,
|
||||
|
|
@ -28,7 +28,7 @@ body:json {
|
|||
},
|
||||
{
|
||||
"id": 456,
|
||||
"date": "2025-12-23",
|
||||
"date": "2025-12-25",
|
||||
"time": "11:30",
|
||||
"requirements": {
|
||||
"capacity": 0.75,
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import io.github.js0ny.ilp_coursework.data.request.MovementRequest;
|
|||
import io.github.js0ny.ilp_coursework.data.request.RegionCheckRequest;
|
||||
import io.github.js0ny.ilp_coursework.service.GpsCalculationService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
|
@ -27,8 +25,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
@RequestMapping("/api/v1")
|
||||
public class ApiController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiController.class);
|
||||
|
||||
private final GpsCalculationService gpsService;
|
||||
|
||||
/**
|
||||
|
|
@ -49,7 +45,6 @@ public class ApiController {
|
|||
*/
|
||||
@GetMapping("/uid")
|
||||
public String getUid() {
|
||||
log.info("GET /api/v1/uid");
|
||||
return "s2522255";
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +58,6 @@ public class ApiController {
|
|||
public double getDistance(@RequestBody DistanceRequest request) {
|
||||
LngLat position1 = request.position1();
|
||||
LngLat position2 = request.position2();
|
||||
log.info("POST /api/v1/distanceTo position1={} position2={}", position1, position2);
|
||||
return gpsService.calculateDistance(position1, position2);
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +72,6 @@ public class ApiController {
|
|||
public boolean getIsCloseTo(@RequestBody DistanceRequest request) {
|
||||
LngLat position1 = request.position1();
|
||||
LngLat position2 = request.position2();
|
||||
log.info("POST /api/v1/isCloseTo position1={} position2={}", position1, position2);
|
||||
return gpsService.isCloseTo(position1, position2);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +86,6 @@ public class ApiController {
|
|||
public LngLat getNextPosition(@RequestBody MovementRequest request) {
|
||||
LngLat start = request.start();
|
||||
Angle angle = new Angle(request.angle());
|
||||
log.info("POST /api/v1/nextPosition start={} angle={}", start, angle);
|
||||
return gpsService.nextPosition(start, angle);
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +99,6 @@ public class ApiController {
|
|||
public boolean getIsInRegion(@RequestBody RegionCheckRequest request) {
|
||||
LngLat position = request.position();
|
||||
Region region = request.region();
|
||||
log.info("POST /api/v1/isInRegion position={} region={}", position, region);
|
||||
return gpsService.checkIsInRegion(position, region);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
|
|||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||
import io.github.js0ny.ilp_coursework.service.PathFinderService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
|
@ -25,8 +23,6 @@ import java.util.List;
|
|||
@RequestMapping("/api/v1")
|
||||
public class DroneController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DroneController.class);
|
||||
|
||||
private final DroneInfoService droneInfoService;
|
||||
private final DroneAttrComparatorService droneAttrComparatorService;
|
||||
private final PathFinderService pathFinderService;
|
||||
|
|
@ -59,7 +55,6 @@ public class DroneController {
|
|||
*/
|
||||
@GetMapping("/dronesWithCooling/{state}")
|
||||
public List<String> getDronesWithCoolingCapability(@PathVariable boolean state) {
|
||||
log.info("GET /api/v1/dronesWithCooling/{}", state);
|
||||
return droneInfoService.dronesWithCooling(state);
|
||||
}
|
||||
|
||||
|
|
@ -73,11 +68,9 @@ public class DroneController {
|
|||
@GetMapping("/droneDetails/{id}")
|
||||
public ResponseEntity<Drone> getDroneDetail(@PathVariable String id) {
|
||||
try {
|
||||
log.info("GET /api/v1/droneDetails/{}", id);
|
||||
Drone drone = droneInfoService.droneDetail(id);
|
||||
return ResponseEntity.ok(drone);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("GET /api/v1/droneDetails/{} not found", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -93,35 +86,26 @@ public class DroneController {
|
|||
@GetMapping("/queryAsPath/{attrName}/{attrVal}")
|
||||
public List<String> getIdByAttrMap(
|
||||
@PathVariable String attrName, @PathVariable String attrVal) {
|
||||
log.info("GET /api/v1/queryAsPath/{}/{}", attrName, attrVal);
|
||||
return droneAttrComparatorService.dronesWithAttribute(attrName, attrVal);
|
||||
}
|
||||
|
||||
@PostMapping("/query")
|
||||
public List<String> getIdByAttrMapPost(@RequestBody AttrQueryRequest[] attrComparators) {
|
||||
int count = attrComparators == null ? 0 : attrComparators.length;
|
||||
log.info("POST /api/v1/query comparators={}", count);
|
||||
return droneAttrComparatorService.dronesSatisfyingAttributes(attrComparators);
|
||||
}
|
||||
|
||||
@PostMapping("/queryAvailableDrones")
|
||||
public List<String> queryAvailableDrones(@RequestBody MedDispatchRecRequest[] records) {
|
||||
int count = records == null ? 0 : records.length;
|
||||
log.info("POST /api/v1/queryAvailableDrones records={}", count);
|
||||
return droneInfoService.dronesMatchesRequirements(records);
|
||||
}
|
||||
|
||||
@PostMapping("/calcDeliveryPath")
|
||||
public DeliveryPathResponse calculateDeliveryPath(@RequestBody MedDispatchRecRequest[] record) {
|
||||
int count = record == null ? 0 : record.length;
|
||||
log.info("POST /api/v1/calcDeliveryPath records={}", count);
|
||||
return pathFinderService.calculateDeliveryPath(record);
|
||||
}
|
||||
|
||||
@PostMapping("/calcDeliveryPathAsGeoJson")
|
||||
public String calculateDeliveryPathAsGeoJson(@RequestBody MedDispatchRecRequest[] record) {
|
||||
int count = record == null ? 0 : record.length;
|
||||
log.info("POST /api/v1/calcDeliveryPathAsGeoJson records={}", count);
|
||||
return pathFinderService.calculateDeliveryPathAsGeoJson(record);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
package io.github.js0ny.ilp_coursework.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class MapMetaController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MapMetaController.class);
|
||||
|
||||
private final DroneInfoService droneInfoService;
|
||||
|
||||
public MapMetaController(DroneInfoService droneInfoService) {
|
||||
|
|
@ -26,13 +22,11 @@ public class MapMetaController {
|
|||
|
||||
@GetMapping("/restrictedAreas")
|
||||
public List<RestrictedArea> getRestrictedAreas() {
|
||||
log.info("GET /api/v1/restrictedAreas");
|
||||
return droneInfoService.fetchRestrictedAreas();
|
||||
}
|
||||
|
||||
@GetMapping("/servicePoints")
|
||||
public List<ServicePoint> getServicePoints() {
|
||||
log.info("GET /api/v1/servicePoints");
|
||||
return droneInfoService.fetchServicePoints();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,10 +18,13 @@ import java.util.Map;
|
|||
* }
|
||||
*/
|
||||
|
||||
public record DroneEvent(String droneId, double latitude, double longitude, String timestamp) {
|
||||
|
||||
static final int STEP = 1; // seconds between events
|
||||
public record DroneEvent(
|
||||
String droneId,
|
||||
double latitude,
|
||||
double longitude,
|
||||
String timestamp) {
|
||||
|
||||
final static int STEP = 1; // seconds between events
|
||||
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
||||
|
||||
public static List<DroneEvent> fromPathResponse(DeliveryPathResponse resp) {
|
||||
|
|
@ -31,7 +34,11 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,15 +47,19 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri
|
|||
|
||||
// Helper method that converts from DeliveryPathResponse to List<DroneEvent>
|
||||
// with base timestamp
|
||||
public static List<DroneEvent> fromPathResponseWithTimestamp(
|
||||
DeliveryPathResponse resp, LocalDateTime baseTimestamp) {
|
||||
public static List<DroneEvent> fromPathResponseWithTimestamp(DeliveryPathResponse resp,
|
||||
LocalDateTime baseTimestamp) {
|
||||
List<DroneEvent> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -64,11 +75,14 @@ public record DroneEvent(String droneId, double latitude, double longitude, Stri
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
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;
|
||||
|
|
@ -50,13 +54,15 @@ public class DroneInfoService {
|
|||
/**
|
||||
* Return an array of ids of drones with/without cooling capability
|
||||
*
|
||||
* <p>Associated service method with {@code /dronesWithCooling/{state}}
|
||||
* <p>
|
||||
* 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<String> dronesWithCooling(boolean state) {
|
||||
// URI droneUrl = URI.create(baseUrl).resolve(dronesEndpoint);
|
||||
|
|
@ -76,13 +82,16 @@ public class DroneInfoService {
|
|||
/**
|
||||
* Return a {@link Drone}-style json data structure with the given {@code id}
|
||||
*
|
||||
* <p>Associated service method with {@code /droneDetails/{id}}
|
||||
* <p>
|
||||
* 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) {
|
||||
|
|
@ -99,10 +108,12 @@ 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
|
||||
*
|
||||
* <p>Associated service method with
|
||||
* <p>
|
||||
* Associated service method with
|
||||
*
|
||||
* @param rec array of medical dispatch records
|
||||
* @return List of drone ids that match all the requirements
|
||||
|
|
@ -124,10 +135,9 @@ 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());
|
||||
}
|
||||
|
|
@ -135,11 +145,13 @@ 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();
|
||||
|
|
@ -179,13 +191,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();
|
||||
|
|
@ -204,8 +216,7 @@ 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) {
|
||||
|
|
@ -222,8 +233,7 @@ 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) {
|
||||
|
|
@ -246,24 +256,22 @@ public class DroneInfoService {
|
|||
|
||||
public List<RestrictedArea> 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<ServicePoint> 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<ServicePointDrones> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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;
|
||||
|
|
@ -32,6 +31,7 @@ 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,8 +44,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)
|
||||
*/
|
||||
|
|
@ -65,11 +67,14 @@ 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) {
|
||||
|
|
@ -82,8 +87,7 @@ public class PathFinderService {
|
|||
|
||||
this.drones = droneInfoService.fetchAllDrones();
|
||||
List<ServicePoint> servicePoints = droneInfoService.fetchServicePoints();
|
||||
List<ServicePointDrones> servicePointAssignments =
|
||||
droneInfoService.fetchDronesForServicePoints();
|
||||
List<ServicePointDrones> servicePointAssignments = droneInfoService.fetchDronesForServicePoints();
|
||||
List<RestrictedArea> restrictedAreas = droneInfoService.fetchRestrictedAreas();
|
||||
|
||||
this.droneById = this.drones.stream().collect(Collectors.toMap(Drone::id, drone -> drone));
|
||||
|
|
@ -101,17 +105,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.
|
||||
|
|
@ -157,17 +161,14 @@ public class PathFinderService {
|
|||
continue;
|
||||
}
|
||||
|
||||
List<MedDispatchRecRequest> sortedDeliveries =
|
||||
entry.getValue().stream()
|
||||
.sorted(
|
||||
Comparator.comparingDouble(
|
||||
rec ->
|
||||
gpsCalculationService.calculateDistance(
|
||||
servicePointLocation, rec.delivery())))
|
||||
.toList();
|
||||
List<MedDispatchRecRequest> sortedDeliveries = entry.getValue().stream()
|
||||
.sorted(
|
||||
Comparator.comparingDouble(
|
||||
rec -> 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) {
|
||||
TripResult result = buildTrip(drone, servicePointLocation, trip);
|
||||
|
|
@ -194,9 +195,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) {
|
||||
|
|
@ -245,8 +246,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.
|
||||
|
|
@ -265,7 +268,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.
|
||||
|
|
@ -290,9 +294,8 @@ public class PathFinderService {
|
|||
continue;
|
||||
}
|
||||
|
||||
double distance =
|
||||
gpsCalculationService.calculateDistance(
|
||||
servicePointLocation, record.delivery());
|
||||
double distance = gpsCalculationService.calculateDistance(
|
||||
servicePointLocation, record.delivery());
|
||||
|
||||
if (distance < bestScore) {
|
||||
bestScore = distance;
|
||||
|
|
@ -306,14 +309,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<List<MedDispatchRecRequest>> splitTrips(
|
||||
List<MedDispatchRecRequest> deliveries, Drone drone, LngLat servicePoint) {
|
||||
|
|
@ -349,13 +354,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
|
||||
*/
|
||||
|
|
@ -395,10 +402,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);
|
||||
|
||||
|
|
@ -406,11 +412,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<MedDispatchRecRequest> deliveries) {
|
||||
|
|
@ -429,10 +436,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)
|
||||
|
|
@ -462,18 +470,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<Angle> candidateAngles = buildAngleCandidates(desiredAngle);
|
||||
for (Angle angle : candidateAngles) {
|
||||
LngLat next = gpsCalculationService.nextPosition(current, angle);
|
||||
|
|
@ -485,8 +495,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.
|
||||
|
|
@ -521,15 +533,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<LngLat> 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.
|
||||
|
|
@ -542,8 +556,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) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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 static final Logger log = LoggerFactory.getLogger(TelemetryService.class);
|
||||
|
||||
private final HttpClient client;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
|
|
@ -28,8 +24,7 @@ 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) {
|
||||
|
|
@ -39,16 +34,15 @@ 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<Integer, LocalDateTime> deliveryTimestamps) {
|
||||
public void sendEventAsyncByPathResponse(DeliveryPathResponse resp,
|
||||
Map<Integer, LocalDateTime> deliveryTimestamps) {
|
||||
var events = DroneEvent.fromPathResponseWithTimestamps(resp, deliveryTimestamps);
|
||||
for (var event : events) {
|
||||
sendEventAsync(event);
|
||||
|
|
@ -56,24 +50,22 @@ public class TelemetryService {
|
|||
}
|
||||
|
||||
public void sendEventAsync(DroneEvent event) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
String json = mapper.writeValueAsString(event);
|
||||
log.debug("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();
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
||||
client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send telemetry event: {}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
|||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
||||
import io.github.js0ny.ilp_coursework.data.response.DeliveryPathResponse;
|
||||
import io.github.js0ny.ilp_coursework.service.DroneAttrComparatorService;
|
||||
import io.github.js0ny.ilp_coursework.service.DroneInfoService;
|
||||
import io.github.js0ny.ilp_coursework.service.PathFinderService;
|
||||
|
|
@ -410,79 +409,4 @@ public class DroneControllerTest {
|
|||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /calcDeliveryPath")
|
||||
class PostCalcDeliveryPathTests {
|
||||
|
||||
final String API_ENDPOINT = "/api/v1/calcDeliveryPath";
|
||||
|
||||
@Test
|
||||
@DisplayName("Example -> 200 OK")
|
||||
void postCalcDeliveryPath_shouldReturn200AndJson_whenExampleRequest() throws Exception {
|
||||
var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
||||
var delivery = new LngLat(-3.00, 55.121);
|
||||
var record =
|
||||
new MedDispatchRecRequest(
|
||||
123,
|
||||
LocalDate.parse("2025-12-22"),
|
||||
LocalTime.parse("14:30"),
|
||||
reqs,
|
||||
delivery);
|
||||
MedDispatchRecRequest[] requestBody = {record};
|
||||
|
||||
var flightPath = List.of(new LngLat(-3.0, 55.12), new LngLat(-3.01, 55.13));
|
||||
var deliveryPath =
|
||||
new DeliveryPathResponse.DronePath.Delivery(123, flightPath);
|
||||
var dronePath = new DeliveryPathResponse.DronePath(1, List.of(deliveryPath));
|
||||
DeliveryPathResponse expected =
|
||||
new DeliveryPathResponse(
|
||||
12.5f, 42, new DeliveryPathResponse.DronePath[] {dronePath});
|
||||
|
||||
when(pathFinderService.calculateDeliveryPath(any(MedDispatchRecRequest[].class)))
|
||||
.thenReturn(expected);
|
||||
|
||||
mockMvc.perform(
|
||||
post(API_ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestBody)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /calcDeliveryPathAsGeoJson")
|
||||
class PostCalcDeliveryPathAsGeoJsonTests {
|
||||
|
||||
final String API_ENDPOINT = "/api/v1/calcDeliveryPathAsGeoJson";
|
||||
|
||||
@Test
|
||||
@DisplayName("Example -> 200 OK")
|
||||
void postCalcDeliveryPathAsGeoJson_shouldReturn200AndGeoJson_whenExampleRequest()
|
||||
throws Exception {
|
||||
var reqs = new MedDispatchRecRequest.MedRequirement(0.75f, false, true, 13.5f);
|
||||
var delivery = new LngLat(-3.00, 55.121);
|
||||
var record =
|
||||
new MedDispatchRecRequest(
|
||||
123,
|
||||
LocalDate.parse("2025-12-22"),
|
||||
LocalTime.parse("14:30"),
|
||||
reqs,
|
||||
delivery);
|
||||
MedDispatchRecRequest[] requestBody = {record};
|
||||
String expected = "{\"type\":\"FeatureCollection\",\"features\":[]}";
|
||||
|
||||
when(pathFinderService.calculateDeliveryPathAsGeoJson(
|
||||
any(MedDispatchRecRequest[].class)))
|
||||
.thenReturn(expected);
|
||||
|
||||
mockMvc.perform(
|
||||
post(API_ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestBody)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
package io.github.js0ny.ilp_coursework.controller;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.AltitudeRange;
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
||||
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.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@WebMvcTest(MapMetaController.class)
|
||||
public class MapMetaControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
@MockitoBean private DroneInfoService droneInfoService;
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /restrictedAreas")
|
||||
class RestrictedAreasTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("-> 200 OK")
|
||||
void getRestrictedAreas_shouldReturn200AndJson() throws Exception {
|
||||
String endpoint = "/api/v1/restrictedAreas";
|
||||
RestrictedArea area =
|
||||
new RestrictedArea(
|
||||
"Zone A",
|
||||
1,
|
||||
new AltitudeRange(0.0, 120.0),
|
||||
new LngLatAlt[] {
|
||||
new LngLatAlt(0.0, 0.0, 0.0),
|
||||
new LngLatAlt(1.0, 0.0, 0.0),
|
||||
new LngLatAlt(1.0, 1.0, 0.0),
|
||||
new LngLatAlt(0.0, 1.0, 0.0)
|
||||
});
|
||||
List<RestrictedArea> expected = List.of(area);
|
||||
when(droneInfoService.fetchRestrictedAreas()).thenReturn(expected);
|
||||
|
||||
var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
||||
mock.andExpect(status().isOk());
|
||||
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /servicePoints")
|
||||
class ServicePointsTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("-> 200 OK")
|
||||
void getServicePoints_shouldReturn200AndJson() throws Exception {
|
||||
String endpoint = "/api/v1/servicePoints";
|
||||
ServicePoint point = new ServicePoint("Point A", 1, new LngLatAlt(0.1, 0.2, 12.0));
|
||||
List<ServicePoint> expected = List.of(point);
|
||||
when(droneInfoService.fetchServicePoints()).thenReturn(expected);
|
||||
|
||||
var mock = mockMvc.perform(get(endpoint).contentType(MediaType.APPLICATION_JSON));
|
||||
mock.andExpect(status().isOk());
|
||||
mock.andExpect(content().json(objectMapper.writeValueAsString(expected)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
package io.github.js0ny.ilp_coursework.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
|
||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.request.AttrQueryRequest;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class DroneAttrComparatorServiceTest {
|
||||
|
||||
private DroneAttrComparatorService service;
|
||||
private MockRestServiceServer server;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DroneAttrComparatorService();
|
||||
RestTemplate restTemplate =
|
||||
(RestTemplate) ReflectionTestUtils.getField(service, "restTemplate");
|
||||
server = MockRestServiceServer.createServer(restTemplate);
|
||||
ReflectionTestUtils.setField(service, "baseUrl", "http://localhost/");
|
||||
objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("dronesWithAttribute(String, String) tests")
|
||||
class DronesWithAttributeTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return matching ids for boolean attribute")
|
||||
void dronesWithAttribute_shouldReturnMatchingIds() throws Exception {
|
||||
Drone[] drones = {
|
||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
|
||||
new Drone("Drone 2", "2", new DroneCapability(false, true, 5, 500, 1, 1, 1)),
|
||||
new Drone("Drone 3", "3", new DroneCapability(true, false, 12, 800, 1, 1, 1))
|
||||
};
|
||||
String responseBody = objectMapper.writeValueAsString(drones);
|
||||
server.expect(requestTo("http://localhost/drones"))
|
||||
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
|
||||
|
||||
var result = service.dronesWithAttribute("cooling", "true");
|
||||
|
||||
assertThat(result).containsExactly("1", "3");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("dronesSatisfyingAttributes(AttrQueryRequest[]) tests")
|
||||
class DronesSatisfyingAttributesTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return intersection of all rules")
|
||||
void dronesSatisfyingAttributes_shouldReturnIntersection() throws Exception {
|
||||
Drone[] drones = {
|
||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1)),
|
||||
new Drone("Drone 2", "2", new DroneCapability(true, true, 3, 1000, 1, 1, 1)),
|
||||
new Drone("Drone 3", "3", new DroneCapability(true, false, 12, 1000, 1, 1, 1))
|
||||
};
|
||||
String responseBody = objectMapper.writeValueAsString(drones);
|
||||
server.expect(requestTo("http://localhost/drones"))
|
||||
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
|
||||
server.expect(requestTo("http://localhost/drones"))
|
||||
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
|
||||
|
||||
AttrQueryRequest[] comparators = {
|
||||
new AttrQueryRequest("capacity", ">", "5"),
|
||||
new AttrQueryRequest("heating", "=", "true")
|
||||
};
|
||||
|
||||
var result = service.dronesSatisfyingAttributes(comparators);
|
||||
|
||||
assertThat(result).containsExactly("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty list when no comparators")
|
||||
void dronesSatisfyingAttributes_shouldReturnEmpty_whenNoComparators() {
|
||||
AttrQueryRequest[] comparators = {};
|
||||
|
||||
var result = service.dronesSatisfyingAttributes(comparators);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.github.js0ny.ilp_coursework.data.common.AltitudeRange;
|
||||
import io.github.js0ny.ilp_coursework.data.common.DroneAvailability;
|
||||
import io.github.js0ny.ilp_coursework.data.common.DroneCapability;
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLat;
|
||||
import io.github.js0ny.ilp_coursework.data.common.LngLatAlt;
|
||||
import io.github.js0ny.ilp_coursework.data.common.TimeWindow;
|
||||
import io.github.js0ny.ilp_coursework.data.external.Drone;
|
||||
import io.github.js0ny.ilp_coursework.data.external.RestrictedArea;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePoint;
|
||||
import io.github.js0ny.ilp_coursework.data.external.ServicePointDrones;
|
||||
import io.github.js0ny.ilp_coursework.data.request.MedDispatchRecRequest;
|
||||
|
||||
|
|
@ -234,145 +230,4 @@ public class DroneInfoServiceTest {
|
|||
assertThat(resultEmpty).containsExactly("1", "2", "3");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("droneMatchesRequirement(Drone, MedDispatchRecRequest) tests")
|
||||
class DroneMatchesRequirementTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw when requirements are null")
|
||||
void droneMatchesRequirement_shouldThrow_whenRequirementsNull() {
|
||||
Drone drone =
|
||||
new Drone("Drone 1", "1", new DroneCapability(true, true, 10, 1000, 1, 1, 1));
|
||||
MedDispatchRecRequest record =
|
||||
new MedDispatchRecRequest(
|
||||
1, LocalDate.now(), LocalTime.of(9, 0), null, new LngLat(0, 0));
|
||||
|
||||
assertThatThrownBy(() -> droneInfoService.droneMatchesRequirement(drone, record))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("requirements cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw when drone capability is null")
|
||||
void droneMatchesRequirement_shouldThrow_whenCapabilityNull() {
|
||||
Drone drone = new Drone("Drone 1", "1", null);
|
||||
MedDispatchRecRequest record =
|
||||
new MedDispatchRecRequest(
|
||||
1,
|
||||
LocalDate.now(),
|
||||
LocalTime.of(9, 0),
|
||||
new MedDispatchRecRequest.MedRequirement(1, false, false, 10),
|
||||
new LngLat(0, 0));
|
||||
|
||||
assertThatThrownBy(() -> droneInfoService.droneMatchesRequirement(drone, record))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("drone capability cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("fetchAllDrones() tests")
|
||||
class FetchAllDronesTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return list when API returns drones")
|
||||
void fetchAllDrones_shouldReturnList_whenApiReturnsDrones() {
|
||||
Drone[] drones = getMockDrones();
|
||||
when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
|
||||
.thenReturn(drones);
|
||||
|
||||
List<Drone> result = droneInfoService.fetchAllDrones();
|
||||
|
||||
assertThat(result).hasSize(3);
|
||||
assertThat(result.get(0).id()).isEqualTo("1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty list when API returns null")
|
||||
void fetchAllDrones_shouldReturnEmptyList_whenApiReturnsNull() {
|
||||
when(restTemplate.getForObject(URI.create(baseUrl + "drones"), Drone[].class))
|
||||
.thenReturn(null);
|
||||
|
||||
List<Drone> result = droneInfoService.fetchAllDrones();
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("fetchRestrictedAreas() tests")
|
||||
class FetchRestrictedAreasTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return restricted areas")
|
||||
void fetchRestrictedAreas_shouldReturnList() {
|
||||
RestrictedArea[] areas = {
|
||||
new RestrictedArea(
|
||||
"Zone A",
|
||||
1,
|
||||
new AltitudeRange(0, 100),
|
||||
new LngLatAlt[] {
|
||||
new LngLatAlt(0, 0, 0),
|
||||
new LngLatAlt(1, 0, 0),
|
||||
new LngLatAlt(1, 1, 0)
|
||||
})
|
||||
};
|
||||
when(restTemplate.getForObject(
|
||||
URI.create(baseUrl + "restricted-areas"), RestrictedArea[].class))
|
||||
.thenReturn(areas);
|
||||
|
||||
List<RestrictedArea> result = droneInfoService.fetchRestrictedAreas();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("Zone A");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("fetchServicePoints() tests")
|
||||
class FetchServicePointsTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return service points")
|
||||
void fetchServicePoints_shouldReturnList() {
|
||||
ServicePoint[] points = {
|
||||
new ServicePoint("Point A", 1, new LngLatAlt(0.1, 0.2, 3.0))
|
||||
};
|
||||
when(restTemplate.getForObject(
|
||||
URI.create(baseUrl + "service-points"), ServicePoint[].class))
|
||||
.thenReturn(points);
|
||||
|
||||
List<ServicePoint> result = droneInfoService.fetchServicePoints();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("Point A");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("fetchDronesForServicePoints() tests")
|
||||
class FetchDronesForServicePointsTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return service point drones")
|
||||
void fetchDronesForServicePoints_shouldReturnList() {
|
||||
TimeWindow[] timeWindows = {
|
||||
new TimeWindow(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(17, 0))
|
||||
};
|
||||
DroneAvailability drone1Avail = new DroneAvailability("1", timeWindows);
|
||||
ServicePointDrones spd =
|
||||
new ServicePointDrones(1, new DroneAvailability[] {drone1Avail});
|
||||
ServicePointDrones[] servicePointDrones = {spd};
|
||||
when(restTemplate.getForObject(
|
||||
URI.create(baseUrl + "drones-for-service-points"),
|
||||
ServicePointDrones[].class))
|
||||
.thenReturn(servicePointDrones);
|
||||
|
||||
List<ServicePointDrones> result = droneInfoService.fetchDronesForServicePoints();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).servicePointId()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue