fix(frontend): will not auto-resize to fit
This commit is contained in:
parent
3509c556a1
commit
acf9d132f7
4 changed files with 217 additions and 23 deletions
|
|
@ -84,6 +84,8 @@ func (s *Server) snapshotHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
results = append(results, event)
|
results = append(results, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Info("Snapshot retrieved", "time", timeParam, "count", len(results))
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||||
slog.Error("Failed to encode response", "error", err)
|
slog.Error("Failed to encode response", "error", err)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
import PlaybackControls from "./components/PlaybackControls.svelte";
|
import PlaybackControls from "./components/PlaybackControls.svelte";
|
||||||
import {
|
import {
|
||||||
buildTimeline,
|
buildTimeline,
|
||||||
|
computeBounds,
|
||||||
computeWallClock,
|
computeWallClock,
|
||||||
boundsKey,
|
|
||||||
colorFor,
|
colorFor,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
let map;
|
let map;
|
||||||
let pathLayer;
|
let pathLayer;
|
||||||
let markerLayer;
|
let markerLayer;
|
||||||
let lastBoundsKey = "";
|
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";
|
||||||
|
|
@ -58,34 +58,64 @@
|
||||||
let status = "System Ready. Waiting for dispatch payload.";
|
let status = "System Ready. Waiting for dispatch payload.";
|
||||||
let snapshotStatus = "";
|
let snapshotStatus = "";
|
||||||
let snapshot = [];
|
let snapshot = [];
|
||||||
|
let positionsNow = [];
|
||||||
|
let snapshotController;
|
||||||
|
let lastSnapshotTime = "";
|
||||||
let tick = 0;
|
let tick = 0;
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let playTimer;
|
let playTimer;
|
||||||
let startTime = new Date().toISOString().slice(0, 16);
|
let startTime = "";
|
||||||
|
let desiredTime = "";
|
||||||
let sidebarOpen = true;
|
let sidebarOpen = true;
|
||||||
|
|
||||||
|
function deriveStartTime(payload) {
|
||||||
|
if (!Array.isArray(payload)) return null;
|
||||||
|
let earliest = Number.POSITIVE_INFINITY;
|
||||||
|
for (const rec of payload) {
|
||||||
|
if (!rec?.date || !rec?.time) continue;
|
||||||
|
const ts = Date.parse(`${rec.date}T${rec.time}`);
|
||||||
|
if (!Number.isNaN(ts)) {
|
||||||
|
earliest = Math.min(earliest, ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(earliest)) return null;
|
||||||
|
return new Date(earliest).toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = deriveStartTime(defaultDispatch) || new Date().toISOString().slice(0, 16);
|
||||||
|
desiredTime = startTime;
|
||||||
|
|
||||||
|
function jumpToTime(timeValue) {
|
||||||
|
if (!timeValue) return;
|
||||||
|
const target = Date.parse(timeValue);
|
||||||
|
if (Number.isNaN(target)) return;
|
||||||
|
// Reset mission base to the chosen time so the progress bar reflects that date.
|
||||||
|
startTime = timeValue;
|
||||||
|
desiredTime = timeValue;
|
||||||
|
tick = 0;
|
||||||
|
fetchSnapshotForTime(new Date(target).toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
// Reactive Statements
|
// Reactive Statements
|
||||||
$: timeline = buildTimeline(plannedPath);
|
$: timeline = buildTimeline(plannedPath);
|
||||||
$: totalSteps = Math.max(timeline.totalSteps, 1);
|
$: totalSteps = Math.max(timeline.totalSteps, plannedPath?.totalMoves || 1, 1);
|
||||||
$: tick = Math.min(tick, totalSteps - 1);
|
$: tick = Math.min(tick, totalSteps - 1);
|
||||||
$: playbackSeconds = tick * STEP_SECONDS;
|
$: playbackSeconds = tick * STEP_SECONDS;
|
||||||
$: wallClock = computeWallClock(startTime, playbackSeconds);
|
$: wallClock = computeWallClock(startTime, playbackSeconds);
|
||||||
|
$: fetchSnapshotForTime(wallClock, true);
|
||||||
// Compute positions based on current tick
|
$: positionsNow = snapshotToPositions(snapshot);
|
||||||
$: positionsNow = timeline.drones.map((drone) => ({
|
$: currentBounds =
|
||||||
...drone,
|
positionsNow.length > 0
|
||||||
current: drone.path[Math.min(tick, drone.path.length - 1)],
|
? computeBounds(
|
||||||
visited: drone.path.slice(0, Math.min(tick + 1, drone.path.length)),
|
positionsNow
|
||||||
}));
|
.map((d) => d.current)
|
||||||
|
.filter((p) => p && typeof p.lat === "number" && typeof p.lng === "number"),
|
||||||
|
)
|
||||||
|
: timeline.bounds || fallbackBounds;
|
||||||
|
|
||||||
// Update Map
|
// Update Map
|
||||||
$: if (map && palette.length > 0) {
|
$: if (map && palette.length > 0) {
|
||||||
const nextKey = boundsKey(timeline.bounds);
|
|
||||||
if (nextKey && nextKey !== lastBoundsKey) {
|
|
||||||
fitMapToBounds();
|
|
||||||
lastBoundsKey = nextKey;
|
|
||||||
}
|
|
||||||
refreshMap(positionsNow);
|
refreshMap(positionsNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +199,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const derivedStart = deriveStartTime(payload);
|
||||||
|
if (derivedStart) {
|
||||||
|
startTime = derivedStart;
|
||||||
|
desiredTime = derivedStart;
|
||||||
|
}
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
status = "Transmitting dispatch to API...";
|
status = "Transmitting dispatch to API...";
|
||||||
stopPlaying();
|
stopPlaying();
|
||||||
|
|
@ -183,6 +219,7 @@
|
||||||
|
|
||||||
plannedPath = await res.json();
|
plannedPath = await res.json();
|
||||||
tick = 0;
|
tick = 0;
|
||||||
|
fitMapToBounds();
|
||||||
status = "Flight path received. Ready for simulation.";
|
status = "Flight path received. Ready for simulation.";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
plannedPath = samplePathResponse;
|
plannedPath = samplePathResponse;
|
||||||
|
|
@ -197,6 +234,12 @@
|
||||||
status = "Loaded sample simulation data.";
|
status = "Loaded sample simulation data.";
|
||||||
tick = 0;
|
tick = 0;
|
||||||
stopPlaying();
|
stopPlaying();
|
||||||
|
fitMapToBounds();
|
||||||
|
const sampleStart = deriveStartTime(defaultDispatch);
|
||||||
|
if (sampleStart) {
|
||||||
|
startTime = sampleStart;
|
||||||
|
desiredTime = sampleStart;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
|
|
@ -218,23 +261,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSnapshot() {
|
async function fetchSnapshot() {
|
||||||
snapshotStatus = "Querying Black Box...";
|
await fetchSnapshotForTime(wallClock, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSnapshotForTime(requestedTime = wallClock, silent = false) {
|
||||||
|
if (!requestedTime) return;
|
||||||
|
|
||||||
|
if (silent && requestedTime === lastSnapshotTime) return;
|
||||||
|
|
||||||
|
snapshotStatus = silent
|
||||||
|
? `Syncing snapshot for ${requestedTime}`
|
||||||
|
: `Querying Black Box for ${requestedTime}...`;
|
||||||
|
lastSnapshotTime = requestedTime;
|
||||||
|
|
||||||
|
if (snapshotController) snapshotController.abort();
|
||||||
|
const controller = new AbortController();
|
||||||
|
snapshotController = controller;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${blackBoxBase}/snapshot?time=${encodeURIComponent(wallClock)}`,
|
`${blackBoxBase}/snapshot?time=${encodeURIComponent(requestedTime)}`,
|
||||||
|
{ signal: controller.signal },
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error(res.status);
|
if (!res.ok) throw new Error(res.status);
|
||||||
snapshot = await res.json();
|
const data = await res.json();
|
||||||
snapshotStatus = `Retrieved ${snapshot.length} events for ${wallClock}`;
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
snapshot = data;
|
||||||
|
snapshotStatus = `Retrieved ${snapshot.length} events for ${requestedTime}`;
|
||||||
|
} else {
|
||||||
|
// Graceful fallback: project planned position if no events exist at the requested time.
|
||||||
|
const fallback = (timeline?.drones || [])
|
||||||
|
.map((d, idx) => {
|
||||||
|
const coords = d.path || [];
|
||||||
|
if (!coords.length) return null;
|
||||||
|
const pos = coords[Math.min(tick, coords.length - 1)];
|
||||||
|
return {
|
||||||
|
droneId: d.id ?? idx + 1,
|
||||||
|
latitude: pos.lat,
|
||||||
|
longitude: pos.lng,
|
||||||
|
timestamp: requestedTime,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
snapshot = fallback;
|
||||||
|
snapshotStatus = `No telemetry at ${requestedTime}; showing planned positions.`;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err?.name === "AbortError") return;
|
||||||
snapshotStatus = `Connection Error: ${err.message}`;
|
snapshotStatus = `Connection Error: ${err.message}`;
|
||||||
snapshot = [];
|
snapshot = [];
|
||||||
|
lastSnapshotTime = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitMapToBounds() {
|
function fitMapToBounds() {
|
||||||
if (!map || !timeline.bounds) return;
|
if (!map || !currentBounds) return;
|
||||||
const b = timeline.bounds;
|
const b = currentBounds;
|
||||||
map.fitBounds(
|
map.fitBounds(
|
||||||
[
|
[
|
||||||
[b.minLat, b.minLng],
|
[b.minLat, b.minLng],
|
||||||
|
|
@ -332,6 +414,60 @@
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme.update((t) => (t === "dark" ? "light" : "dark"));
|
theme.update((t) => (t === "dark" ? "light" : "dark"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshotToPositions(data = snapshot) {
|
||||||
|
const timelinePaths = new Map(
|
||||||
|
(timeline?.drones || []).map((d) => [String(d.id), d.path || []]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const findClosestIndex = (coords, point) => {
|
||||||
|
if (!coords?.length || !point) return -1;
|
||||||
|
let bestIdx = -1;
|
||||||
|
let bestDist = Number.POSITIVE_INFINITY;
|
||||||
|
coords.forEach((c, idx) => {
|
||||||
|
const dist =
|
||||||
|
Math.abs(c.lat - point.lat) + Math.abs(c.lng - point.lng);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
bestIdx = idx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bestIdx;
|
||||||
|
};
|
||||||
|
|
||||||
|
return data
|
||||||
|
.map((event, idx) => {
|
||||||
|
const lat =
|
||||||
|
typeof event.latitude === "number"
|
||||||
|
? event.latitude
|
||||||
|
: typeof event.lat === "number"
|
||||||
|
? event.lat
|
||||||
|
: null;
|
||||||
|
const lng =
|
||||||
|
typeof event.longitude === "number"
|
||||||
|
? event.longitude
|
||||||
|
: typeof event.lng === "number"
|
||||||
|
? event.lng
|
||||||
|
: null;
|
||||||
|
if (lat === null || lng === null) return null;
|
||||||
|
const id = event.droneId ?? event.drone_id ?? idx + 1;
|
||||||
|
const point = { lat, lng };
|
||||||
|
const coords = timelinePaths.get(String(id)) || [];
|
||||||
|
const matchedIdx = findClosestIndex(coords, point);
|
||||||
|
const visited =
|
||||||
|
matchedIdx >= 0
|
||||||
|
? coords.slice(0, matchedIdx + 1)
|
||||||
|
: [point];
|
||||||
|
const fullPath = coords.length ? coords : [point];
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
path: fullPath,
|
||||||
|
visited,
|
||||||
|
current: visited[visited.length - 1] || point,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -393,8 +529,10 @@
|
||||||
{totalSteps}
|
{totalSteps}
|
||||||
{snapshotStatus}
|
{snapshotStatus}
|
||||||
{snapshot}
|
{snapshot}
|
||||||
|
bind:desiredTime
|
||||||
on:togglePlay={togglePlay}
|
on:togglePlay={togglePlay}
|
||||||
on:seek={stopPlaying}
|
on:seek={stopPlaying}
|
||||||
on:fetchSnapshot={fetchSnapshot}
|
on:fetchSnapshot={fetchSnapshot}
|
||||||
|
on:setTime={(e) => jumpToTime(e.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
export let totalSteps = 0;
|
export let totalSteps = 0;
|
||||||
export let snapshotStatus = "";
|
export let snapshotStatus = "";
|
||||||
export let snapshot = [];
|
export let snapshot = [];
|
||||||
|
export let desiredTime = "";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
|
@ -120,13 +121,31 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 text-xs mt-1">
|
||||||
|
<label class="text-slate-500 dark:text-slate-400 uppercase tracking-wide"
|
||||||
|
>Jump to Time</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
bind:value={desiredTime}
|
||||||
|
class="bg-white/60 dark:bg-black/30 border border-slate-300 dark:border-slate-700 rounded-md px-2 py-1 font-mono text-xs text-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-500/40 focus:border-sky-500"
|
||||||
|
on:change={(e) => dispatch('setTime', e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-xs font-semibold rounded-md transition-all text-slate-800 dark:text-slate-100 bg-sky-500/20 hover:bg-sky-500/30"
|
||||||
|
on:click={() => dispatch('setTime', desiredTime)}
|
||||||
|
>
|
||||||
|
GO
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if snapshot.length > 0}
|
{#if snapshot.length > 0}
|
||||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||||
{#each snapshot.slice(0, 3) as event}
|
{#each snapshot.slice(0, 3) as event}
|
||||||
<div
|
<div
|
||||||
class="text-xs px-2 py-0.5 rounded-full bg-slate-500/20 font-mono text-slate-600 dark:text-slate-300"
|
class="text-xs px-2 py-0.5 rounded-full bg-slate-500/20 font-mono text-slate-600 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
Drone {event.drone_id} log found
|
Drone {event.droneId ?? event.drone_id ?? "?"} log found
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if snapshot.length > 3}
|
{#if snapshot.length > 3}
|
||||||
|
|
|
||||||
35
ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru
Normal file
35
ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
meta {
|
||||||
|
name: Imperative
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{API_BASE}}/calcDeliveryPath
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"date": "2025-11-22",
|
||||||
|
"time": "09:30",
|
||||||
|
"requirements": {
|
||||||
|
"capacity": 0.75,
|
||||||
|
"heating": false,
|
||||||
|
"maxCost": 13.5
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"lng": -3.167074009381139,
|
||||||
|
"lat": 55.94740195123114
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue