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
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
@ -393,8 +529,10 @@
|
|||
{totalSteps}
|
||||
{snapshotStatus}
|
||||
{snapshot}
|
||||
bind:desiredTime
|
||||
on:togglePlay={togglePlay}
|
||||
on:seek={stopPlaying}
|
||||
on:fetchSnapshot={fetchSnapshot}
|
||||
on:setTime={(e) => jumpToTime(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
export let totalSteps = 0;
|
||||
export let snapshotStatus = "";
|
||||
export let snapshot = [];
|
||||
export let desiredTime = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -120,13 +121,31 @@
|
|||
</button>
|
||||
</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}
|
||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||
{#each snapshot.slice(0, 3) as event}
|
||||
<div
|
||||
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>
|
||||
{/each}
|
||||
{#if snapshot.length > 3}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue