feat(frontend): Add restricted area visualisation

This commit is contained in:
js0ny 2025-12-06 05:55:59 +00:00
parent 47993d66bc
commit 83bb72faac
13 changed files with 1589 additions and 1465 deletions

View file

@ -48,12 +48,16 @@
let map;
let pathLayer;
let markerLayer;
let restrictedLayer;
let servicePointLayer;
let currentBounds = fallbackBounds;
let apiBase = "http://localhost:8080/api/v1";
let blackBoxBase = "http://localhost:3000";
let dispatchBody = JSON.stringify(defaultDispatch, null, 2);
let plannedPath = samplePathResponse;
let restrictedAreas = [];
let servicePoints = [];
let stepSeconds = STEP_SECONDS;
let status = "System Ready. Waiting for dispatch payload.";
@ -101,7 +105,11 @@
// Reactive Statements
$: 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);
$: playbackSeconds = tick * stepSeconds;
$: wallClock = computeWallClock(startTime, playbackSeconds);
@ -112,7 +120,12 @@
? computeBounds(
positionsNow
.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;
@ -178,9 +191,13 @@
pathLayer = L.layerGroup().addTo(map);
markerLayer = L.layerGroup().addTo(map);
restrictedLayer = L.layerGroup().addTo(map);
servicePointLayer = L.layerGroup().addTo(map);
fitMapToBounds();
refreshMap();
loadRestrictedAreas();
loadServicePoints();
return () => map?.remove();
});
@ -223,7 +240,40 @@
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 (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() {
theme.update((t) => (t === "dark" ? "light" : "dark"));
}
@ -410,9 +504,7 @@
const coords = timelinePaths.get(String(id)) || [];
const matchedIdx = findClosestIndex(coords, point);
const visited =
matchedIdx >= 0
? coords.slice(0, matchedIdx + 1)
: [point];
matchedIdx >= 0 ? coords.slice(0, matchedIdx + 1) : [point];
const fullPath = coords.length ? coords : [point];
return {
id,
@ -477,7 +569,6 @@
<PlaybackControls
{wallClock}
{playbackSeconds}
totalCost={plannedPath?.totalCost}
activeDrones={plannedPath?.dronePaths?.length}
{isPlaying}
bind:tick

View file

@ -24,40 +24,49 @@
}
.leaflet-container {
background: #111 !important;
background: #111 !important;
}
.drone-tooltip {
@apply bg-gray-800 text-white p-2 rounded-md border-gray-700 border;
@apply bg-gray-800 text-white p-2 rounded-md border-gray-700 border;
}
/* Arrow should be the same color as the tooltip background */
.drone-tooltip.leaflet-tooltip-top::before {
border-top-color: theme('colors.gray.800');
border-top-color: theme("colors.gray.800");
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.3); /* Slate 400 with opacity */
border-radius: 20px;
background-color: rgba(148, 163, 184, 0.3); /* Slate 400 with opacity */
border-radius: 20px;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(148, 163, 184, 0.5);
background-color: rgba(148, 163, 184, 0.5);
}
/* Drone Popup Styles */
.drone-popup .leaflet-popup-content-wrapper {
@apply bg-slate-900/90 backdrop-blur-md border border-slate-700 text-slate-100 rounded-lg p-0 shadow-xl;
@apply bg-slate-900/90 backdrop-blur-md border border-slate-700 text-slate-100 rounded-lg p-0 shadow-xl;
}
.drone-popup .leaflet-popup-tip {
@apply bg-slate-900/90 border-slate-700;
@apply bg-slate-900/90 border-slate-700;
}
.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);
}

View file

@ -4,7 +4,6 @@
export let wallClock = "";
export let playbackSeconds = 0;
export let totalCost = 0;
export let activeDrones = 0;
export let isPlaying = false;
export let tick = 0;
@ -48,22 +47,13 @@
>{formatDuration(playbackSeconds)}</span
>
</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">
<span class="text-xs uppercase text-slate-500 dark:text-slate-400"
>Active Drones</span
>
<span
class="font-mono text-xl font-bold text-slate-800 dark:text-slate-100"
>{activeDrones ?? 0}</span
>{snapshot.length ?? 0}</span
>
</div>
</div>

View file

@ -23,7 +23,9 @@
</script>
<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">
<h1
@ -90,11 +92,27 @@
{/if}
</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"
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>
</div>
</div>
@ -105,7 +123,9 @@
> {status}
</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">
<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"
@ -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"
/>
</div>
</div>
{/if}
</div>
@ -207,15 +226,16 @@
></textarea>
</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
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}
>
{loading ? "CALCULATING..." : "REQUEST PATH"}
</button>
</div>
</div>
</aside>

View file

@ -1,8 +1,8 @@
import './app.css';
import App from './App.svelte';
import "./app.css";
import App from "./App.svelte";
const app = new App({
target: document.getElementById('app')
target: document.getElementById("app"),
});
export default app;

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
import { writable } from 'svelte/store';
import { writable } from "svelte/store";
export const theme = writable('dark');
export const theme = writable("dark");

View file

@ -1,108 +1,103 @@
import { fallbackBounds } from "./sampleData.js";
export function buildTimeline(response) {
const drones = [];
const allPoints = [];
let longest = 0;
const drones = [];
const allPoints = [];
let longest = 0;
if (!response || !response.dronePaths) {
return { drones, totalSteps: 0, bounds: fallbackBounds };
}
if (!response || !response.dronePaths) {
return { drones, totalSteps: 0, bounds: fallbackBounds };
}
for (const path of response.dronePaths) {
const coords = flattenDeliveries(path.deliveries);
if (!coords.length) continue;
drones.push({ id: path.droneId, path: coords });
coords.forEach((p) => allPoints.push(p));
longest = Math.max(longest, coords.length);
}
for (const path of response.dronePaths) {
const coords = flattenDeliveries(path.deliveries);
if (!coords.length) continue;
drones.push({ id: path.droneId, path: coords });
coords.forEach((p) => allPoints.push(p));
longest = Math.max(longest, coords.length);
}
// Add padding points for bounds calculation if empty
const basePoints =
allPoints.length > 0
? allPoints
: [
{
lng: fallbackBounds.minLng,
lat: fallbackBounds.minLat,
},
{
lng: fallbackBounds.maxLng,
lat: fallbackBounds.maxLat,
},
];
// Add padding points for bounds calculation if empty
const basePoints =
allPoints.length > 0
? allPoints
: [
{
lng: fallbackBounds.minLng,
lat: fallbackBounds.minLat,
},
{
lng: fallbackBounds.maxLng,
lat: fallbackBounds.maxLat,
},
];
return {
drones,
totalSteps: longest || 0,
bounds: computeBounds(basePoints),
};
return {
drones,
totalSteps: longest || 0,
bounds: computeBounds(basePoints),
};
}
function flattenDeliveries(deliveries) {
const coords = [];
for (const delivery of deliveries || []) {
for (const point of delivery.flightPath || []) {
if (
!coords.length ||
!samePoint(coords[coords.length - 1], point)
) {
coords.push(point);
}
}
const coords = [];
for (const delivery of deliveries || []) {
for (const point of delivery.flightPath || []) {
if (!coords.length || !samePoint(coords[coords.length - 1], point)) {
coords.push(point);
}
}
return coords;
}
return coords;
}
function samePoint(a, b) {
return Math.abs(a.lng - b.lng) < 1e-9 && Math.abs(a.lat - b.lat) < 1e-9;
return Math.abs(a.lng - b.lng) < 1e-9 && Math.abs(a.lat - b.lat) < 1e-9;
}
export function computeBounds(points) {
if (!points.length) return fallbackBounds;
let minLng = Infinity,
maxLng = -Infinity,
minLat = Infinity,
maxLat = -Infinity;
for (const p of points) {
minLng = Math.min(minLng, p.lng);
maxLng = Math.max(maxLng, p.lng);
minLat = Math.min(minLat, p.lat);
maxLat = Math.max(maxLat, p.lat);
}
const padding = 0.002; // Slightly more padding
return {
minLng: minLng - padding,
maxLng: maxLng + padding,
minLat: minLat - padding,
maxLat: maxLat + padding,
};
if (!points.length) return fallbackBounds;
let minLng = Infinity,
maxLng = -Infinity,
minLat = Infinity,
maxLat = -Infinity;
for (const p of points) {
minLng = Math.min(minLng, p.lng);
maxLng = Math.max(maxLng, p.lng);
minLat = Math.min(minLat, p.lat);
maxLat = Math.max(maxLat, p.lat);
}
const padding = 0.002; // Slightly more padding
return {
minLng: minLng - padding,
maxLng: maxLng + padding,
minLat: minLat - padding,
maxLat: maxLat + padding,
};
}
export function colorFor(idx, palette) {
if (!palette || palette.length === 0) return "#000";
return palette[idx % palette.length];
if (!palette || palette.length === 0) return "#000";
return palette[idx % palette.length];
}
export function formatDuration(sec) {
const m = Math.floor(sec / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(sec % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
const m = Math.floor(sec / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(sec % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
}
export function computeWallClock(start, seconds) {
const base = start ? new Date(start) : new Date();
const ts = new Date(base.getTime() + seconds * 1000);
return isNaN(ts.getTime())
? new Date().toISOString()
: ts.toISOString();
const base = start ? new Date(start) : new Date();
const ts = new Date(base.getTime() + seconds * 1000);
return isNaN(ts.getTime()) ? new Date().toISOString() : ts.toISOString();
}
export function boundsKey(bounds) {
if (!bounds) return "";
return `${bounds.minLat}:${bounds.maxLat}:${bounds.minLng}:${bounds.maxLng}`;
if (!bounds) return "";
return `${bounds.minLat}:${bounds.maxLat}:${bounds.minLng}:${bounds.maxLng}`;
}

View file

@ -38,6 +38,7 @@
bun
svelte-language-server
typescript-language-server
prettier
];
shellHook = ''
export JAVA_HOME=${pkgs.jdk21}

View file

@ -25,6 +25,20 @@ body:json {
"lng": -3.167074009381139,
"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
}
}
]
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -54,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);
@ -80,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) {
@ -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
*
* <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
@ -128,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());
}
@ -139,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();
@ -183,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();
@ -208,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) {
@ -226,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) {
@ -250,31 +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<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() {
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);
}