feat(frontend): Add restricted area visualisation
This commit is contained in:
parent
47993d66bc
commit
83bb72faac
13 changed files with 1589 additions and 1465 deletions
|
|
@ -48,12 +48,16 @@
|
||||||
let map;
|
let map;
|
||||||
let pathLayer;
|
let pathLayer;
|
||||||
let markerLayer;
|
let markerLayer;
|
||||||
|
let restrictedLayer;
|
||||||
|
let servicePointLayer;
|
||||||
let currentBounds = fallbackBounds;
|
let currentBounds = fallbackBounds;
|
||||||
|
|
||||||
let apiBase = "http://localhost:8080/api/v1";
|
let apiBase = "http://localhost:8080/api/v1";
|
||||||
let blackBoxBase = "http://localhost:3000";
|
let blackBoxBase = "http://localhost:3000";
|
||||||
let dispatchBody = JSON.stringify(defaultDispatch, null, 2);
|
let dispatchBody = JSON.stringify(defaultDispatch, null, 2);
|
||||||
let plannedPath = samplePathResponse;
|
let plannedPath = samplePathResponse;
|
||||||
|
let restrictedAreas = [];
|
||||||
|
let servicePoints = [];
|
||||||
let stepSeconds = STEP_SECONDS;
|
let stepSeconds = STEP_SECONDS;
|
||||||
|
|
||||||
let status = "System Ready. Waiting for dispatch payload.";
|
let status = "System Ready. Waiting for dispatch payload.";
|
||||||
|
|
@ -101,7 +105,11 @@
|
||||||
|
|
||||||
// Reactive Statements
|
// Reactive Statements
|
||||||
$: timeline = buildTimeline(plannedPath);
|
$: timeline = buildTimeline(plannedPath);
|
||||||
$: totalSteps = Math.max(timeline.totalSteps, plannedPath?.totalMoves || 1, 1);
|
$: totalSteps = Math.max(
|
||||||
|
timeline.totalSteps,
|
||||||
|
plannedPath?.totalMoves || 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
$: tick = Math.min(tick, totalSteps - 1);
|
$: tick = Math.min(tick, totalSteps - 1);
|
||||||
$: playbackSeconds = tick * stepSeconds;
|
$: playbackSeconds = tick * stepSeconds;
|
||||||
$: wallClock = computeWallClock(startTime, playbackSeconds);
|
$: wallClock = computeWallClock(startTime, playbackSeconds);
|
||||||
|
|
@ -112,7 +120,12 @@
|
||||||
? computeBounds(
|
? computeBounds(
|
||||||
positionsNow
|
positionsNow
|
||||||
.map((d) => d.current)
|
.map((d) => d.current)
|
||||||
.filter((p) => p && typeof p.lat === "number" && typeof p.lng === "number"),
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
p &&
|
||||||
|
typeof p.lat === "number" &&
|
||||||
|
typeof p.lng === "number",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: timeline.bounds || fallbackBounds;
|
: timeline.bounds || fallbackBounds;
|
||||||
|
|
||||||
|
|
@ -178,9 +191,13 @@
|
||||||
|
|
||||||
pathLayer = L.layerGroup().addTo(map);
|
pathLayer = L.layerGroup().addTo(map);
|
||||||
markerLayer = L.layerGroup().addTo(map);
|
markerLayer = L.layerGroup().addTo(map);
|
||||||
|
restrictedLayer = L.layerGroup().addTo(map);
|
||||||
|
servicePointLayer = L.layerGroup().addTo(map);
|
||||||
|
|
||||||
fitMapToBounds();
|
fitMapToBounds();
|
||||||
refreshMap();
|
refreshMap();
|
||||||
|
loadRestrictedAreas();
|
||||||
|
loadServicePoints();
|
||||||
return () => map?.remove();
|
return () => map?.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -223,7 +240,40 @@
|
||||||
await fetchSnapshotForTime(wallClock, false);
|
await fetchSnapshotForTime(wallClock, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSnapshotForTime(requestedTime = wallClock, silent = false) {
|
async function loadRestrictedAreas() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase}/restrictedAreas`);
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(
|
||||||
|
res.statusText || "failed to load restricted areas",
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
restrictedAreas = Array.isArray(data) ? data : [];
|
||||||
|
drawRestrictedAreas();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[RestrictedAreas] fetch failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadServicePoints() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase}/servicePoints`);
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(
|
||||||
|
res.statusText || "failed to load service points",
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
servicePoints = Array.isArray(data) ? data : [];
|
||||||
|
drawServicePoints();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ServicePoints] fetch failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSnapshotForTime(
|
||||||
|
requestedTime = wallClock,
|
||||||
|
silent = false,
|
||||||
|
) {
|
||||||
if (!requestedTime) return;
|
if (!requestedTime) return;
|
||||||
|
|
||||||
if (silent && requestedTime === lastSnapshotTime) return;
|
if (silent && requestedTime === lastSnapshotTime) return;
|
||||||
|
|
@ -366,6 +416,50 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawRestrictedAreas() {
|
||||||
|
if (!restrictedLayer) return;
|
||||||
|
restrictedLayer.clearLayers();
|
||||||
|
restrictedAreas.forEach((area) => {
|
||||||
|
const coords = (area?.vertices || [])
|
||||||
|
.map((v) => [v.lat, v.lng])
|
||||||
|
.filter((p) => Array.isArray(p) && p.length === 2);
|
||||||
|
if (coords.length < 3) return;
|
||||||
|
L.polygon(coords, {
|
||||||
|
color: "#ef4444", // red
|
||||||
|
weight: 2,
|
||||||
|
fill: false,
|
||||||
|
dashArray: "6,6",
|
||||||
|
})
|
||||||
|
.bindTooltip(area?.name || "Restricted Area", { sticky: true })
|
||||||
|
.addTo(restrictedLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawServicePoints() {
|
||||||
|
if (!servicePointLayer) return;
|
||||||
|
servicePointLayer.clearLayers();
|
||||||
|
servicePoints.forEach((sp) => {
|
||||||
|
const loc = sp?.location;
|
||||||
|
if (
|
||||||
|
!loc ||
|
||||||
|
typeof loc.lat !== "number" ||
|
||||||
|
typeof loc.lng !== "number"
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const marker = L.marker([loc.lat, loc.lng], {
|
||||||
|
icon: L.divIcon({
|
||||||
|
className: "service-point-icon",
|
||||||
|
html: `<div class="triangle-marker"></div>`,
|
||||||
|
iconSize: [16, 16],
|
||||||
|
iconAnchor: [8, 8],
|
||||||
|
}),
|
||||||
|
}).bindTooltip(sp?.name || `Service Point ${sp?.id ?? ""}`, {
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
marker.addTo(servicePointLayer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme.update((t) => (t === "dark" ? "light" : "dark"));
|
theme.update((t) => (t === "dark" ? "light" : "dark"));
|
||||||
}
|
}
|
||||||
|
|
@ -410,9 +504,7 @@
|
||||||
const coords = timelinePaths.get(String(id)) || [];
|
const coords = timelinePaths.get(String(id)) || [];
|
||||||
const matchedIdx = findClosestIndex(coords, point);
|
const matchedIdx = findClosestIndex(coords, point);
|
||||||
const visited =
|
const visited =
|
||||||
matchedIdx >= 0
|
matchedIdx >= 0 ? coords.slice(0, matchedIdx + 1) : [point];
|
||||||
? coords.slice(0, matchedIdx + 1)
|
|
||||||
: [point];
|
|
||||||
const fullPath = coords.length ? coords : [point];
|
const fullPath = coords.length ? coords : [point];
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -477,7 +569,6 @@
|
||||||
<PlaybackControls
|
<PlaybackControls
|
||||||
{wallClock}
|
{wallClock}
|
||||||
{playbackSeconds}
|
{playbackSeconds}
|
||||||
totalCost={plannedPath?.totalCost}
|
|
||||||
activeDrones={plannedPath?.dronePaths?.length}
|
activeDrones={plannedPath?.dronePaths?.length}
|
||||||
{isPlaying}
|
{isPlaying}
|
||||||
bind:tick
|
bind:tick
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
/* Arrow should be the same color as the tooltip background */
|
/* Arrow should be the same color as the tooltip background */
|
||||||
.drone-tooltip.leaflet-tooltip-top::before {
|
.drone-tooltip.leaflet-tooltip-top::before {
|
||||||
border-top-color: theme('colors.gray.800');
|
border-top-color: theme("colors.gray.800");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
|
|
@ -61,3 +61,12 @@
|
||||||
.drone-popup .leaflet-popup-content {
|
.drone-popup .leaflet-popup-content {
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-point-icon .triangle-marker {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 10px solid #0ea5e9; /* sky-500 */
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
export let wallClock = "";
|
export let wallClock = "";
|
||||||
export let playbackSeconds = 0;
|
export let playbackSeconds = 0;
|
||||||
export let totalCost = 0;
|
|
||||||
export let activeDrones = 0;
|
export let activeDrones = 0;
|
||||||
export let isPlaying = false;
|
export let isPlaying = false;
|
||||||
export let tick = 0;
|
export let tick = 0;
|
||||||
|
|
@ -48,22 +47,13 @@
|
||||||
>{formatDuration(playbackSeconds)}</span
|
>{formatDuration(playbackSeconds)}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
|
|
||||||
>Total Cost</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
|
|
||||||
>{totalCost ?? "—"}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
|
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
|
||||||
>Active Drones</span
|
>Active Drones</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
|
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
|
||||||
>{activeDrones ?? 0}</span
|
>{snapshot.length ?? 0}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="absolute top-0 left-0 bottom-0 w-[380px] pointer-events-auto bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-2xl flex flex-col m-4 rounded-2xl p-5 gap-5 transition-transform duration-500 ease-in-out {open ? 'translate-x-0' : '-translate-x-[120%]'}"
|
class="absolute top-0 left-0 bottom-0 w-[380px] pointer-events-auto bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-2xl flex flex-col m-4 rounded-2xl p-5 gap-5 transition-transform duration-500 ease-in-out {open
|
||||||
|
? 'translate-x-0'
|
||||||
|
: '-translate-x-[120%]'}"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1
|
<h1
|
||||||
|
|
@ -90,11 +92,27 @@
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => dispatch('close')}
|
on:click={() => dispatch("close")}
|
||||||
class="p-2 rounded-lg text-slate-500 hover:text-sky-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
class="p-2 rounded-lg text-slate-500 hover:text-sky-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||||
aria-label="Close Sidebar"
|
aria-label="Close Sidebar"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><line x1="18" y1="6" x2="6" y2="18"></line><line
|
||||||
|
x1="6"
|
||||||
|
y1="6"
|
||||||
|
x2="18"
|
||||||
|
y2="18"
|
||||||
|
></line></svg
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,7 +123,9 @@
|
||||||
> {status}
|
> {status}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3 overflow-y-auto pr-1 custom-scrollbar flex-1 flex flex-col">
|
<div
|
||||||
|
class="space-y-3 overflow-y-auto pr-1 custom-scrollbar flex-1 flex flex-col"
|
||||||
|
>
|
||||||
<div class="border-b border-slate-200 dark:border-slate-800 pb-2">
|
<div class="border-b border-slate-200 dark:border-slate-800 pb-2">
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center justify-between text-sm font-semibold tracking-wider uppercase text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors focus:outline-none"
|
class="w-full flex items-center justify-between text-sm font-semibold tracking-wider uppercase text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors focus:outline-none"
|
||||||
|
|
@ -189,7 +209,6 @@
|
||||||
class="w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2 rounded-lg font-mono text-xs transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20"
|
class="w-full bg-white/50 dark:bg-black/30 border border-slate-300 dark:border-slate-700 text-slate-800 dark:text-slate-100 p-2 rounded-lg font-mono text-xs transition-all focus:outline-none focus:border-sky-500 focus:bg-white dark:focus:bg-black/50 focus:ring-2 focus:ring-sky-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,15 +226,16 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 mt-auto pt-4 border-t border-slate-200 dark:border-slate-800">
|
<div
|
||||||
|
class="flex gap-2 mt-auto pt-4 border-t border-slate-200 dark:border-slate-800"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="flex-1 p-2.5 rounded-lg font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed text-white bg-sky-500 shadow-[0_4px_12px_theme(colors.sky.500/30)] hover:bg-sky-600 disabled:hover:bg-sky-500"
|
class="flex-1 p-2.5 rounded-lg font-semibold text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed text-white bg-sky-500 shadow-[0_4px_12px_theme(colors.sky.500/30)] hover:bg-sky-600 disabled:hover:bg-sky-500"
|
||||||
on:click={() => dispatch('request')}
|
on:click={() => dispatch("request")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? "CALCULATING..." : "REQUEST PATH"}
|
{loading ? "CALCULATING..." : "REQUEST PATH"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import './app.css';
|
import "./app.css";
|
||||||
import App from './App.svelte';
|
import App from "./App.svelte";
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById('app')
|
target: document.getElementById("app"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export const theme = writable('dark');
|
export const theme = writable("dark");
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,7 @@ function flattenDeliveries(deliveries) {
|
||||||
const coords = [];
|
const coords = [];
|
||||||
for (const delivery of deliveries || []) {
|
for (const delivery of deliveries || []) {
|
||||||
for (const point of delivery.flightPath || []) {
|
for (const point of delivery.flightPath || []) {
|
||||||
if (
|
if (!coords.length || !samePoint(coords[coords.length - 1], point)) {
|
||||||
!coords.length ||
|
|
||||||
!samePoint(coords[coords.length - 1], point)
|
|
||||||
) {
|
|
||||||
coords.push(point);
|
coords.push(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,9 +94,7 @@ export function formatDuration(sec) {
|
||||||
export function computeWallClock(start, seconds) {
|
export function computeWallClock(start, seconds) {
|
||||||
const base = start ? new Date(start) : new Date();
|
const base = start ? new Date(start) : new Date();
|
||||||
const ts = new Date(base.getTime() + seconds * 1000);
|
const ts = new Date(base.getTime() + seconds * 1000);
|
||||||
return isNaN(ts.getTime())
|
return isNaN(ts.getTime()) ? new Date().toISOString() : ts.toISOString();
|
||||||
? new Date().toISOString()
|
|
||||||
: ts.toISOString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function boundsKey(bounds) {
|
export function boundsKey(bounds) {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
bun
|
bun
|
||||||
svelte-language-server
|
svelte-language-server
|
||||||
typescript-language-server
|
typescript-language-server
|
||||||
|
prettier
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export JAVA_HOME=${pkgs.jdk21}
|
export JAVA_HOME=${pkgs.jdk21}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,20 @@ body:json {
|
||||||
"lng": -3.167074009381139,
|
"lng": -3.167074009381139,
|
||||||
"lat": 55.94740195123114
|
"lat": 55.94740195123114
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"date": "2025-11-22",
|
||||||
|
"time": "09:35",
|
||||||
|
"requirements": {
|
||||||
|
"capacity": 0.85,
|
||||||
|
"heating": true,
|
||||||
|
"maxCost": 13.5
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"lng": -3.1891515471685636,
|
||||||
|
"lat": 55.95347060952466
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package io.github.js0ny.ilp_coursework.controller;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1")
|
|
||||||
public class GeoJsonDataController {
|
|
||||||
|
|
||||||
private final DroneInfoService droneInfoService;
|
|
||||||
|
|
||||||
public GeoJsonDataController(DroneInfoService droneInfoService) {
|
|
||||||
this.droneInfoService = droneInfoService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/getAllRestrictedAreaByGeoJson")
|
|
||||||
public String getAllRestrictedAreaGeoJson() throws JsonProcessingException {
|
|
||||||
return droneInfoService.fetchRestrictedAreasInGeoJson().stream().reduce("", String::concat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1")
|
||||||
|
public class MapMetaController {
|
||||||
|
|
||||||
|
private final DroneInfoService droneInfoService;
|
||||||
|
|
||||||
|
public MapMetaController(DroneInfoService droneInfoService) {
|
||||||
|
this.droneInfoService = droneInfoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/restrictedAreas")
|
||||||
|
public List<RestrictedArea> getRestrictedAreas() {
|
||||||
|
return droneInfoService.fetchRestrictedAreas();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/servicePoints")
|
||||||
|
public List<ServicePoint> getServicePoints() {
|
||||||
|
return droneInfoService.fetchServicePoints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,10 +54,12 @@ public class DroneInfoService {
|
||||||
/**
|
/**
|
||||||
* Return an array of ids of drones with/without cooling capability
|
* Return an array of ids of drones with/without cooling capability
|
||||||
*
|
*
|
||||||
* <p>Associated service method with {@code /dronesWithCooling/{state}}
|
* <p>
|
||||||
|
* Associated service method with {@code /dronesWithCooling/{state}}
|
||||||
*
|
*
|
||||||
* @param state determines the capability filtering
|
* @param state determines the capability filtering
|
||||||
* @return if {@code state} is true, return ids of drones with cooling capability, else without
|
* @return if {@code state} is true, return ids of drones with cooling
|
||||||
|
* capability, else without
|
||||||
* cooling
|
* cooling
|
||||||
* @see
|
* @see
|
||||||
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
* io.github.js0ny.ilp_coursework.controller.DroneController#getDronesWithCoolingCapability(boolean)
|
||||||
|
|
@ -80,12 +82,15 @@ public class DroneInfoService {
|
||||||
/**
|
/**
|
||||||
* Return a {@link Drone}-style json data structure with the given {@code id}
|
* Return a {@link Drone}-style json data structure with the given {@code id}
|
||||||
*
|
*
|
||||||
* <p>Associated service method with {@code /droneDetails/{id}}
|
* <p>
|
||||||
|
* Associated service method with {@code /droneDetails/{id}}
|
||||||
*
|
*
|
||||||
* @param id The id of the drone
|
* @param id The id of the drone
|
||||||
* @return drone json body of given id
|
* @return drone json body of given id
|
||||||
* @throws NullPointerException when cannot fetch available drones from remote
|
* @throws NullPointerException when cannot fetch available drones from
|
||||||
* @throws IllegalArgumentException when drone with given {@code id} cannot be found this should
|
* remote
|
||||||
|
* @throws IllegalArgumentException when drone with given {@code id} cannot be
|
||||||
|
* found this should
|
||||||
* lead to a 404
|
* lead to a 404
|
||||||
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
* @see io.github.js0ny.ilp_coursework.controller.DroneController#getDroneDetail(String)
|
||||||
*/
|
*/
|
||||||
|
|
@ -103,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
|
* records
|
||||||
*
|
*
|
||||||
* <p>Associated service method with
|
* <p>
|
||||||
|
* Associated service method with
|
||||||
*
|
*
|
||||||
* @param rec array of medical dispatch records
|
* @param rec array of medical dispatch records
|
||||||
* @return List of drone ids that match all the requirements
|
* @return List of drone ids that match all the requirements
|
||||||
|
|
@ -128,8 +135,7 @@ public class DroneInfoService {
|
||||||
return drones.stream()
|
return drones.stream()
|
||||||
.filter(d -> d != null && d.capability() != null)
|
.filter(d -> d != null && d.capability() != null)
|
||||||
.filter(
|
.filter(
|
||||||
d ->
|
d -> Arrays.stream(rec)
|
||||||
Arrays.stream(rec)
|
|
||||||
.filter(r -> r != null && r.requirements() != null)
|
.filter(r -> r != null && r.requirements() != null)
|
||||||
.allMatch(r -> droneMatchesRequirement(d, r)))
|
.allMatch(r -> droneMatchesRequirement(d, r)))
|
||||||
.map(Drone::id)
|
.map(Drone::id)
|
||||||
|
|
@ -142,8 +148,10 @@ public class DroneInfoService {
|
||||||
* @param drone the drone to be checked
|
* @param drone the drone to be checked
|
||||||
* @param record the medical dispatch record containing the requirement
|
* @param record the medical dispatch record containing the requirement
|
||||||
* @return true if the drone meets the requirement, false otherwise
|
* @return true if the drone meets the requirement, false otherwise
|
||||||
* @throws IllegalArgumentException when record requirements or drone capability is invalid
|
* @throws IllegalArgumentException when record requirements or drone capability
|
||||||
* (capacity and id cannot be null in {@code MedDispathRecDto})
|
* is invalid
|
||||||
|
* (capacity and id cannot be null in
|
||||||
|
* {@code MedDispathRecDto})
|
||||||
*/
|
*/
|
||||||
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
public boolean droneMatchesRequirement(Drone drone, MedDispatchRecRequest record) {
|
||||||
var requirements = record.requirements();
|
var requirements = record.requirements();
|
||||||
|
|
@ -183,13 +191,13 @@ public class DroneInfoService {
|
||||||
* Helper to check if a drone is available at the required date and time
|
* Helper to check if a drone is available at the required date and time
|
||||||
*
|
*
|
||||||
* @param droneId the id of the drone to be checked
|
* @param droneId the id of the drone to be checked
|
||||||
* @param record the medical dispatch record containing the required date and time
|
* @param record the medical dispatch record containing the required date and
|
||||||
|
* time
|
||||||
* @return true if the drone is available, false otherwise
|
* @return true if the drone is available, false otherwise
|
||||||
*/
|
*/
|
||||||
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
private boolean checkAvailability(String droneId, MedDispatchRecRequest record) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePoints =
|
ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
|
||||||
|
|
||||||
LocalDate requiredDate = record.date();
|
LocalDate requiredDate = record.date();
|
||||||
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
DayOfWeek requiredDay = requiredDate.getDayOfWeek();
|
||||||
|
|
@ -208,8 +216,7 @@ public class DroneInfoService {
|
||||||
|
|
||||||
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
private LngLat queryServicePointLocationByDroneId(String droneId) {
|
||||||
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI droneUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePoints =
|
ServicePointDrones[] servicePoints = restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
||||||
restTemplate.getForObject(droneUrl, ServicePointDrones[].class);
|
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -226,8 +233,7 @@ public class DroneInfoService {
|
||||||
private LngLat queryServicePointLocation(int id) {
|
private LngLat queryServicePointLocation(int id) {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
|
|
||||||
ServicePoint[] servicePoints =
|
ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
|
||||||
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
for (var sp : servicePoints) {
|
for (var sp : servicePoints) {
|
||||||
|
|
@ -250,31 +256,22 @@ public class DroneInfoService {
|
||||||
|
|
||||||
public List<RestrictedArea> fetchRestrictedAreas() {
|
public List<RestrictedArea> fetchRestrictedAreas() {
|
||||||
URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
|
URI restrictedUrl = URI.create(baseUrl).resolve(restrictedAreasEndpoint);
|
||||||
RestrictedArea[] restrictedAreas =
|
RestrictedArea[] restrictedAreas = restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
|
||||||
restTemplate.getForObject(restrictedUrl, RestrictedArea[].class);
|
|
||||||
assert restrictedAreas != null;
|
assert restrictedAreas != null;
|
||||||
return Arrays.asList(restrictedAreas);
|
return Arrays.asList(restrictedAreas);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> fetchRestrictedAreasInGeoJson() throws JsonProcessingException {
|
|
||||||
var mapper = new ObjectMapper();
|
|
||||||
var ras = fetchRestrictedAreas();
|
|
||||||
var geoJson = ras.stream().map(RestrictedArea::toRegion).map(Region::toGeoJson).toList();
|
|
||||||
return Collections.singletonList(mapper.writeValueAsString(geoJson));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ServicePoint> fetchServicePoints() {
|
public List<ServicePoint> fetchServicePoints() {
|
||||||
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
URI servicePointUrl = URI.create(baseUrl).resolve(servicePointsEndpoint);
|
||||||
ServicePoint[] servicePoints =
|
ServicePoint[] servicePoints = restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
||||||
restTemplate.getForObject(servicePointUrl, ServicePoint[].class);
|
|
||||||
assert servicePoints != null;
|
assert servicePoints != null;
|
||||||
return Arrays.asList(servicePoints);
|
return Arrays.asList(servicePoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
public List<ServicePointDrones> fetchDronesForServicePoints() {
|
||||||
URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
URI servicePointDronesUrl = URI.create(baseUrl).resolve(dronesForServicePointsEndpoint);
|
||||||
ServicePointDrones[] servicePointDrones =
|
ServicePointDrones[] servicePointDrones = restTemplate.getForObject(servicePointDronesUrl,
|
||||||
restTemplate.getForObject(servicePointDronesUrl, ServicePointDrones[].class);
|
ServicePointDrones[].class);
|
||||||
assert servicePointDrones != null;
|
assert servicePointDrones != null;
|
||||||
return Arrays.asList(servicePointDrones);
|
return Arrays.asList(servicePointDrones);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue