From acf9d132f7b905c0ac967e6d11df0b31c917f1d5 Mon Sep 17 00:00:00 2001 From: js0ny Date: Sat, 6 Dec 2025 03:54:04 +0000 Subject: [PATCH] fix(frontend): will not auto-resize to fit --- drone-black-box/main.go | 2 + drone-frontend/src/App.svelte | 182 +++++++++++++++--- .../src/components/PlaybackControls.svelte | 21 +- .../ilp-cw-api/Front-end Test/Imperative.bru | 35 ++++ 4 files changed, 217 insertions(+), 23 deletions(-) create mode 100644 ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru diff --git a/drone-black-box/main.go b/drone-black-box/main.go index 62b0e89..70d5898 100644 --- a/drone-black-box/main.go +++ b/drone-black-box/main.go @@ -84,6 +84,8 @@ func (s *Server) snapshotHandler(w http.ResponseWriter, r *http.Request) { results = append(results, event) } + slog.Info("Snapshot retrieved", "time", timeParam, "count", len(results)) + w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(results); err != nil { slog.Error("Failed to encode response", "error", err) diff --git a/drone-frontend/src/App.svelte b/drone-frontend/src/App.svelte index 298e0b6..fd121e3 100644 --- a/drone-frontend/src/App.svelte +++ b/drone-frontend/src/App.svelte @@ -12,8 +12,8 @@ import PlaybackControls from "./components/PlaybackControls.svelte"; import { buildTimeline, + computeBounds, computeWallClock, - boundsKey, colorFor, } from "./utils.js"; @@ -48,7 +48,7 @@ let map; let pathLayer; let markerLayer; - let lastBoundsKey = ""; + let currentBounds = fallbackBounds; let apiBase = "http://localhost:8080/api/v1"; let blackBoxBase = "http://localhost:3000"; @@ -58,34 +58,64 @@ let status = "System Ready. Waiting for dispatch payload."; let snapshotStatus = ""; let snapshot = []; + let positionsNow = []; + let snapshotController; + let lastSnapshotTime = ""; let tick = 0; let isPlaying = false; let loading = false; let playTimer; - let startTime = new Date().toISOString().slice(0, 16); + let startTime = ""; + let desiredTime = ""; 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 $: timeline = buildTimeline(plannedPath); - $: totalSteps = Math.max(timeline.totalSteps, 1); + $: totalSteps = Math.max(timeline.totalSteps, plannedPath?.totalMoves || 1, 1); $: tick = Math.min(tick, totalSteps - 1); $: playbackSeconds = tick * STEP_SECONDS; $: wallClock = computeWallClock(startTime, playbackSeconds); - - // Compute positions based on current tick - $: positionsNow = timeline.drones.map((drone) => ({ - ...drone, - current: drone.path[Math.min(tick, drone.path.length - 1)], - visited: drone.path.slice(0, Math.min(tick + 1, drone.path.length)), - })); + $: fetchSnapshotForTime(wallClock, true); + $: positionsNow = snapshotToPositions(snapshot); + $: currentBounds = + positionsNow.length > 0 + ? computeBounds( + positionsNow + .map((d) => d.current) + .filter((p) => p && typeof p.lat === "number" && typeof p.lng === "number"), + ) + : timeline.bounds || fallbackBounds; // Update Map $: if (map && palette.length > 0) { - const nextKey = boundsKey(timeline.bounds); - if (nextKey && nextKey !== lastBoundsKey) { - fitMapToBounds(); - lastBoundsKey = nextKey; - } refreshMap(positionsNow); } @@ -169,6 +199,12 @@ return; } + const derivedStart = deriveStartTime(payload); + if (derivedStart) { + startTime = derivedStart; + desiredTime = derivedStart; + } + loading = true; status = "Transmitting dispatch to API..."; stopPlaying(); @@ -183,6 +219,7 @@ plannedPath = await res.json(); tick = 0; + fitMapToBounds(); status = "Flight path received. Ready for simulation."; } catch (err) { plannedPath = samplePathResponse; @@ -197,6 +234,12 @@ status = "Loaded sample simulation data."; tick = 0; stopPlaying(); + fitMapToBounds(); + const sampleStart = deriveStartTime(defaultDispatch); + if (sampleStart) { + startTime = sampleStart; + desiredTime = sampleStart; + } } function togglePlay() { @@ -218,23 +261,62 @@ } 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 { 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); - snapshot = await res.json(); - snapshotStatus = `Retrieved ${snapshot.length} events for ${wallClock}`; + const data = await res.json(); + 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) { + if (err?.name === "AbortError") return; snapshotStatus = `Connection Error: ${err.message}`; snapshot = []; + lastSnapshotTime = ""; } } function fitMapToBounds() { - if (!map || !timeline.bounds) return; - const b = timeline.bounds; + if (!map || !currentBounds) return; + const b = currentBounds; map.fitBounds( [ [b.minLat, b.minLng], @@ -332,6 +414,60 @@ function toggleTheme() { 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); + }
jumpToTime(e.detail)} />
diff --git a/drone-frontend/src/components/PlaybackControls.svelte b/drone-frontend/src/components/PlaybackControls.svelte index 3c0adf6..0715d92 100644 --- a/drone-frontend/src/components/PlaybackControls.svelte +++ b/drone-frontend/src/components/PlaybackControls.svelte @@ -11,6 +11,7 @@ export let totalSteps = 0; export let snapshotStatus = ""; export let snapshot = []; + export let desiredTime = ""; const dispatch = createEventDispatcher(); @@ -120,13 +121,31 @@ +
+ + dispatch('setTime', e.target.value)} + /> + +
+ {#if snapshot.length > 0}
{#each snapshot.slice(0, 3) as event}
- Drone {event.drone_id} log found + Drone {event.droneId ?? event.drone_id ?? "?"} log found
{/each} {#if snapshot.length > 3} diff --git a/ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru b/ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru new file mode 100644 index 0000000..32a6e68 --- /dev/null +++ b/ilp-rest-service/ilp-cw-api/Front-end Test/Imperative.bru @@ -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 +}