SAR Search Pattern Update

This commit is contained in:
2026-03-05 20:47:28 +00:00
parent c0b62300f3
commit 42be74328c
7 changed files with 865 additions and 928 deletions

View File

@@ -50,7 +50,7 @@ Returns a JSON array of all indexed simulations, including their ID, name, creat
### `POST /api/simulations/create` ### `POST /api/simulations/create`
Creates a new simulation entry. Creates a new simulation entry.
- **Request Body:** JSON object containing the combined configurations (e.g., `search`, `uav`, `ugv`). The configuration *must* contain at least one search pattern definition (either `spiral`, `lawnmower`, or `levy`) inside the JSON. - **Request Body:** JSON object containing the combined configurations (e.g., `search`, `uav`, `ugv`). The configuration *must* contain at least one search pattern definition (either `spiral`, `lawnmower`, `levy`, or `sar`) inside the JSON.
- **Behavior:** Checks if an identical config exists. If so, it returns the existing simulation ID/Name for overwriting. If not, it allocates a new `simulation_X` incrementally, inserts it into the database, builds the file directory in `../results`, and returns the new ID/Name. Returns `400 Bad Request` if payload is not valid JSON or lacks a known search pattern. - **Behavior:** Checks if an identical config exists. If so, it returns the existing simulation ID/Name for overwriting. If not, it allocates a new `simulation_X` incrementally, inserts it into the database, builds the file directory in `../results`, and returns the new ID/Name. Returns `400 Bad Request` if payload is not valid JSON or lacks a known search pattern.
- **Response:** JSON object `{"id": <int>, "name": <string>}` - **Response:** JSON object `{"id": <int>, "name": <string>}`

View File

@@ -48,12 +48,15 @@ func (rt *Router) CreateSimulation(w http.ResponseWriter, r *http.Request) {
if _, ok := searchMap["levy"]; ok { if _, ok := searchMap["levy"]; ok {
hasPattern = true hasPattern = true
} }
if _, ok := searchMap["sar"]; ok {
hasPattern = true
}
} }
} }
} }
if !hasPattern { if !hasPattern {
http.Error(w, "Simulation configuration must include a search pattern (spiral, lawnmower, or levy)", http.StatusBadRequest) http.Error(w, "Simulation configuration must include a search pattern (spiral, lawnmower, levy, or sar)", http.StatusBadRequest)
return return
} }

View File

@@ -61,6 +61,7 @@
<option value="spiral">Spiral</option> <option value="spiral">Spiral</option>
<option value="lawnmower">Lawnmower</option> <option value="lawnmower">Lawnmower</option>
<option value="levy">Levy</option> <option value="levy">Levy</option>
<option value="sar">SAR</option>
</select> </select>
</div> </div>
</details> </details>

View File

@@ -1,57 +1,59 @@
<script lang="ts"> <script lang="ts">
import { import {
parseConfig, parseConfig,
formatDate, formatDate,
type SimulationState, type SimulationState,
} from "./simulationState.svelte"; } from "./simulationState.svelte";
import { formatTime } from "$lib/ts/utils"; import { formatTime } from "$lib/ts/utils";
let { state }: { state: SimulationState } = $props(); let { state }: { state: SimulationState } = $props();
</script> </script>
<main class="table-content"> <main class="table-content">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Sim Name</th> <th>Sim Name</th>
<th>Search Pattern</th> <th>Search Pattern</th>
<th>Search Time</th> <th>Search Time</th>
<th>Total Time</th> <th>Total Time</th>
<th>Date Run</th> <th>Date Run</th>
<th>Link</th> <th>Link</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each state.paginatedSimulations as sim} {#each state.paginatedSimulations as sim}
<tr> <tr>
<td>{sim.id}</td> <td>{sim.id}</td>
<td>{sim.name}</td> <td>{sim.name}</td>
<td style="text-transform: capitalize;" <td style="text-transform: capitalize;"
>{parseConfig(sim.config).pattern || "Unknown"}</td >{parseConfig(sim.config).pattern === "sar"
> ? "SAR"
<td>{formatTime(sim.search_time || NaN)}</td> : parseConfig(sim.config).pattern || "Unknown"}</td
<td>{formatTime(sim.total_time || NaN)}</td> >
<td>{formatDate(sim.created_at)}</td> <td>{formatTime(sim.search_time || NaN)}</td>
<td><a href={`/simulation/${sim.id}`}>View Details</a></td> <td>{formatTime(sim.total_time || NaN)}</td>
</tr> <td>{formatDate(sim.created_at)}</td>
{/each} <td><a href={`/simulation/${sim.id}`}>View Details</a></td>
</tbody> </tr>
</table> {/each}
</tbody>
</table>
<div class="pagination"> <div class="pagination">
<button <button
onclick={() => state.goToPage(state.currentPage - 1)} onclick={() => state.goToPage(state.currentPage - 1)}
disabled={state.currentPage === 1} disabled={state.currentPage === 1}
> >
Previous Previous
</button> </button>
<span>Page {state.currentPage} of {state.totalPages}</span> <span>Page {state.currentPage} of {state.totalPages}</span>
<button <button
onclick={() => state.goToPage(state.currentPage + 1)} onclick={() => state.goToPage(state.currentPage + 1)}
disabled={state.currentPage === state.totalPages} disabled={state.currentPage === state.totalPages}
> >
Next Next
</button> </button>
</div> </div>
</main> </main>

View File

@@ -29,6 +29,7 @@ export function parseConfig(config: string) {
if (searchObj.spiral) pattern = "spiral"; if (searchObj.spiral) pattern = "spiral";
else if (searchObj.lawnmower) pattern = "lawnmower"; else if (searchObj.lawnmower) pattern = "lawnmower";
else if (searchObj.levy) pattern = "levy"; else if (searchObj.levy) pattern = "levy";
else if (searchObj.sar) pattern = "sar";
return { return {
altitude: searchObj.altitude != null ? parseFloat(searchObj.altitude) : null, altitude: searchObj.altitude != null ? parseFloat(searchObj.altitude) : null,

File diff suppressed because it is too large Load Diff

View File

@@ -1,364 +1,340 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { formatDate } from "$lib/simulationState.svelte"; import { formatDate } from "$lib/simulationState.svelte";
import { import {
type SimulationDetails, type SimulationDetails,
flattenJSON, flattenJSON,
isImage, isImage,
getResourceUrl, getResourceUrl,
formatTime, formatTime,
} from "$lib/ts/utils"; } from "$lib/ts/utils";
let { data } = $props(); let { data } = $props();
let id = $derived(data.id); let id = $derived(data.id);
let simulation = $state<SimulationDetails | null>(null); let simulation = $state<SimulationDetails | null>(null);
let parsedConfig: { key: string; value: string }[] | null = $derived.by( let parsedConfig: { key: string; value: string }[] | null = $derived.by(
() => { () => {
if (!simulation?.config) return null; if (!simulation?.config) return null;
try { try {
let cleanedConfig = simulation.config.replace( let cleanedConfig = simulation.config.replace(
/"([^"]+)\.yaml"\s*:/g, /"([^"]+)\.yaml"\s*:/g,
'"$1":', '"$1":',
); );
return flattenJSON(JSON.parse(cleanedConfig)); return flattenJSON(JSON.parse(cleanedConfig));
} catch { } catch {
return null; return null;
} }
}, },
); );
let searchConfig = $derived( let searchConfig = $derived(
parsedConfig parsedConfig
?.filter((c) => c.key.startsWith("search.")) ?.filter((c) => c.key.startsWith("search."))
.map((c) => ({ ...c, key: c.key.replace("search.", "") })), .map((c) => ({ ...c, key: c.key.replace("search.", "") })),
); );
let uavConfig = $derived( let uavConfig = $derived(
parsedConfig parsedConfig
?.filter((c) => c.key.startsWith("uav.")) ?.filter((c) => c.key.startsWith("uav."))
.map((c) => ({ ...c, key: c.key.replace("uav.", "") })), .map((c) => ({ ...c, key: c.key.replace("uav.", "") })),
); );
let ugvConfig = $derived( let ugvConfig = $derived(
parsedConfig parsedConfig
?.filter((c) => c.key.startsWith("ugv.")) ?.filter((c) => c.key.startsWith("ugv."))
.map((c) => ({ ...c, key: c.key.replace("ugv.", "") })), .map((c) => ({ ...c, key: c.key.replace("ugv.", "") })),
); );
let otherConfig = $derived( let otherConfig = $derived(
parsedConfig?.filter( parsedConfig?.filter(
(c) => (c) =>
!c.key.startsWith("search.") && !c.key.startsWith("search.") &&
!c.key.startsWith("uav.") && !c.key.startsWith("uav.") &&
!c.key.startsWith("ugv."), !c.key.startsWith("ugv."),
), ),
); );
function groupConfig(data: { key: string; value: string }[] | undefined) { function groupConfig(data: { key: string; value: string }[] | undefined) {
if (!data) return {}; if (!data) return {};
const groups: Record<string, { key: string; value: string }[]> = { const groups: Record<string, { key: string; value: string }[]> = {
_general: [], _general: [],
}; };
for (const item of data) { for (const item of data) {
const parts = item.key.split("."); const parts = item.key.split(".");
if (parts.length > 1) { if (parts.length > 1) {
const groupName = parts[0]; const groupName = parts[0];
const subKey = parts.slice(1).join("."); const subKey = parts.slice(1).join(".");
if (!groups[groupName]) groups[groupName] = []; if (!groups[groupName]) groups[groupName] = [];
groups[groupName].push({ key: subKey, value: item.value }); groups[groupName].push({ key: subKey, value: item.value });
} else { } else {
groups["_general"].push(item); groups["_general"].push(item);
} }
} }
return groups; return groups;
} }
let imagesList = $derived<string[]>( let imagesList = $derived<string[]>(
simulation && simulation.resources simulation && simulation.resources
? simulation.resources.filter(isImage) ? simulation.resources.filter(isImage)
: [], : [],
); );
onMount(async () => { onMount(async () => {
try { try {
const res = await fetch(`/api/simulations/${id}`); const res = await fetch(`/api/simulations/${id}`);
if (res.ok) { if (res.ok) {
simulation = await res.json(); simulation = await res.json();
setTimeout(() => { setTimeout(() => {
window.print(); window.print();
}, 500); }, 500);
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
} }
}); });
</script> </script>
{#if simulation} {#if simulation}
<div class="print-container"> <div class="print-container">
<h1>Simulation Report: {simulation.name}</h1> <h1>Simulation Report: {simulation.name}</h1>
<p><strong>ID:</strong> {simulation.id}</p> <p><strong>ID:</strong> {simulation.id}</p>
<p> <p>
<strong>Search Pattern:</strong> <strong>Search Pattern:</strong>
<span style="text-transform: capitalize;"> <span style="text-transform: capitalize;">
{parsedConfig?.find((c) => c.key === "search.spiral.max_legs") {parsedConfig?.find((c) => c.key === "search.spiral.max_legs")
? "Spiral" ? "Spiral"
: parsedConfig?.find( : parsedConfig?.find((c) => c.key === "search.lawnmower.width")
(c) => c.key === "search.lawnmower.width", ? "Lawnmower"
) : parsedConfig?.find((c) => c.key === "search.levy.max_steps")
? "Lawnmower" ? "Levy"
: parsedConfig?.find( : parsedConfig?.find((c) => c.key.startsWith("search.sar"))
(c) => c.key === "search.levy.max_steps", ? "SAR"
) : "Unknown"}
? "Levy" </span>
: "Unknown"} </p>
</span> <p><strong>Date Code:</strong> {formatDate(simulation.created_at)}</p>
</p>
<p><strong>Date Code:</strong> {formatDate(simulation.created_at)}</p>
{#if simulation.search_time !== null && simulation.total_time !== null} {#if simulation.search_time !== null && simulation.total_time !== null}
<p> <p>
<strong>Search Time:</strong> <strong>Search Time:</strong>
{formatTime(simulation.search_time)} | {formatTime(simulation.search_time)} |
<strong>Total Time:</strong> <strong>Total Time:</strong>
{formatTime(simulation.total_time)} {formatTime(simulation.total_time)}
</p> </p>
{/if} {/if}
{#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info} {#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info}
<div <div
class="hardware-box" class="hardware-box"
style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; background-color: #f9f9f9; break-inside: avoid;" style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; background-color: #f9f9f9; break-inside: avoid;"
> >
<h3>Hardware Spec</h3> <h3>Hardware Spec</h3>
<p><strong>CPU:</strong> {simulation.cpu_info || "N/A"}</p> <p><strong>CPU:</strong> {simulation.cpu_info || "N/A"}</p>
<p><strong>GPU:</strong> {simulation.gpu_info || "N/A"}</p> <p><strong>GPU:</strong> {simulation.gpu_info || "N/A"}</p>
<p><strong>RAM:</strong> {simulation.ram_info || "N/A"}</p> <p><strong>RAM:</strong> {simulation.ram_info || "N/A"}</p>
</div> </div>
{/if} {/if}
{#if simulation.config} {#if simulation.config}
<div class="config-box"> <div class="config-box">
<strong>Configuration Options:</strong> <strong>Configuration Options:</strong>
{#snippet ConfigTable( {#snippet ConfigTable(
title: string, title: string,
data: { key: string; value: string }[] | undefined, data: { key: string; value: string }[] | undefined,
)} )}
{#if data && data.length > 0} {#if data && data.length > 0}
{@const groups = groupConfig(data)} {@const groups = groupConfig(data)}
<div class="config-category"> <div class="config-category">
<h2>{title}</h2> <h2>{title}</h2>
<div class="tables-container"> <div class="tables-container">
{#if groups["_general"].length > 0} {#if groups["_general"].length > 0}
<div class="table-wrapper"> <div class="table-wrapper">
<h3 class="table-label">General</h3> <h3 class="table-label">General</h3>
<table class="config-table"> <table class="config-table">
<thead> <thead>
<tr <tr><th>Parameter</th><th>Value</th></tr>
><th>Parameter</th><th </thead>
>Value</th <tbody>
></tr {#each groups["_general"] as item}
> <tr><td>{item.key}</td><td>{item.value}</td></tr>
</thead> {/each}
<tbody> </tbody>
{#each groups["_general"] as item} </table>
<tr </div>
><td>{item.key}</td><td {/if}
>{item.value}</td {#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]}
></tr <div class="table-wrapper">
> <h3 class="table-label" style="text-transform: capitalize;">
{/each} {groupName}
</tbody> </h3>
</table> <table class="config-table">
</div> <thead>
{/if} <tr><th>Parameter</th><th>Value</th></tr>
{#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]} </thead>
<div class="table-wrapper"> <tbody>
<h3 {#each items as item}
class="table-label" <tr><td>{item.key}</td><td>{item.value}</td></tr>
style="text-transform: capitalize;" {/each}
> </tbody>
{groupName} </table>
</h3> </div>
<table class="config-table"> {/each}
<thead> </div>
<tr </div>
><th>Parameter</th><th {/if}
>Value</th {/snippet}
></tr
>
</thead>
<tbody>
{#each items as item}
<tr
><td>{item.key}</td><td
>{item.value}</td
></tr
>
{/each}
</tbody>
</table>
</div>
{/each}
</div>
</div>
{/if}
{/snippet}
<div class="super-container"> <div class="super-container">
{@render ConfigTable("Search", searchConfig)} {@render ConfigTable("Search", searchConfig)}
{@render ConfigTable("UGV", ugvConfig)} {@render ConfigTable("UGV", ugvConfig)}
{@render ConfigTable("UAV", uavConfig)} {@render ConfigTable("UAV", uavConfig)}
{@render ConfigTable("Other", otherConfig)} {@render ConfigTable("Other", otherConfig)}
</div> </div>
</div> </div>
{/if} {/if}
<h2>Visuals</h2> <h2>Visuals</h2>
{#if imagesList.length > 0} {#if imagesList.length > 0}
<div class="image-grid"> <div class="image-grid">
{#each imagesList as image} {#each imagesList as image}
<div class="image-wrapper"> <div class="image-wrapper">
<img <img src={getResourceUrl(simulation.name, image)} alt={image} />
src={getResourceUrl(simulation.name, image)} <p class="caption">{image}</p>
alt={image} </div>
/> {/each}
<p class="caption">{image}</p> </div>
</div> {:else}
{/each} <p>No images found in this simulation.</p>
</div> {/if}
{:else} </div>
<p>No images found in this simulation.</p>
{/if}
</div>
{:else} {:else}
<p>Loading printable report...</p> <p>Loading printable report...</p>
{/if} {/if}
<style> <style>
:global(body) { :global(body) {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
color: #000; color: #000;
background: #fff; background: #fff;
} }
.print-container { .print-container {
max-width: 1000px; max-width: 1000px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
h1 { h1 {
border-bottom: 2px solid #000; border-bottom: 2px solid #000;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 20px; margin-bottom: 20px;
} }
h2 { h2 {
margin-top: 30px; margin-top: 30px;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
h3 { h3 {
margin-bottom: 5px; margin-bottom: 5px;
} }
.config-box { .config-box {
margin-top: 20px; margin-top: 20px;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 15px; padding: 15px;
background-color: #f9f9f9; background-color: #f9f9f9;
break-inside: avoid; break-inside: avoid;
} }
.super-container { .super-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
margin-top: 15px; margin-top: 15px;
} }
.config-category { .config-category {
padding: 15px; padding: 15px;
background: #ffffff; background: #ffffff;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
} }
.config-category h2 { .config-category h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 18px; font-size: 18px;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
padding-bottom: 5px; padding-bottom: 5px;
} }
.tables-container { .tables-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px; gap: 20px;
margin-top: 10px; margin-top: 10px;
} }
.table-wrapper { .table-wrapper {
flex: 1; flex: 1;
min-width: 250px; min-width: 250px;
} }
.config-table { .config-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 14px; font-size: 14px;
} }
.config-table td { .config-table td {
border: 1px solid #888; border: 1px solid #888;
padding: 5px; padding: 5px;
} }
.config-table tr:nth-child(even) { .config-table tr:nth-child(even) {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.image-grid { .image-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px; gap: 20px;
margin-top: 20px; margin-top: 20px;
} }
.image-wrapper { .image-wrapper {
break-inside: avoid; break-inside: avoid;
text-align: center; text-align: center;
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
background: #fafafa; background: #fafafa;
} }
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
display: block; display: block;
margin: 0 auto; margin: 0 auto;
} }
.caption { .caption {
margin-top: 5px; margin-top: 5px;
font-size: 12px; font-size: 12px;
color: #555; color: #555;
} }
@media print { @media print {
@page { @page {
margin: 1cm; margin: 1cm;
} }
.print-container { .print-container {
width: 100%; width: 100%;
max-width: none; max-width: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
} }
</style> </style>