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`
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.
- **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 {
hasPattern = true
}
if _, ok := searchMap["sar"]; ok {
hasPattern = true
}
}
}
}
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
}

View File

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

View File

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

View File

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