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

@@ -28,7 +28,9 @@
<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"
: parseConfig(sim.config).pattern || "Unknown"}</td
> >
<td>{formatTime(sim.search_time || NaN)}</td> <td>{formatTime(sim.search_time || NaN)}</td>
<td>{formatTime(sim.total_time || NaN)}</td> <td>{formatTime(sim.total_time || NaN)}</td>

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,

View File

@@ -102,9 +102,7 @@
simulation && simulation.resources simulation && simulation.resources
? simulation.resources.find( ? simulation.resources.find(
(res: string) => (res: string) =>
res.includes("camera") && res.includes("camera") && !res.includes("ugv") && isVideo(res),
!res.includes("ugv") &&
isVideo(res),
) )
: undefined, : undefined,
); );
@@ -220,9 +218,7 @@
xhr.upload.onprogress = (event) => { xhr.upload.onprogress = (event) => {
if (event.lengthComputable) { if (event.lengthComputable) {
uploadProgress = Math.round( uploadProgress = Math.round((event.loaded / event.total) * 100);
(event.loaded / event.total) * 100,
);
} }
}; };
@@ -230,15 +226,11 @@
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText)); resolve(JSON.parse(xhr.responseText));
} else { } else {
reject( reject(xhr.responseText || `Failed to upload: ${file.name}`);
xhr.responseText ||
`Failed to upload: ${file.name}`,
);
} }
}; };
xhr.onerror = () => xhr.onerror = () => reject(`Network error uploading: ${file.name}`);
reject(`Network error uploading: ${file.name}`);
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
@@ -246,10 +238,7 @@
}); });
if (!simulation.resources.includes(upData.filename)) { if (!simulation.resources.includes(upData.filename)) {
simulation.resources = [ simulation.resources = [...simulation.resources, upData.filename];
...simulation.resources,
upData.filename,
];
} }
} catch (err: any) { } catch (err: any) {
saveError = err.toString(); saveError = err.toString();
@@ -304,9 +293,7 @@
} }
async function handleDeleteResource(resourceName: string) { async function handleDeleteResource(resourceName: string) {
if ( if (
!confirm( !confirm(`Are you sure you want to permanently delete ${resourceName}?`)
`Are you sure you want to permanently delete ${resourceName}?`,
)
) )
return; return;
const res = await fetch( const res = await fetch(
@@ -337,14 +324,10 @@
{#if !isEditing} {#if !isEditing}
<h1>Simulation Detail: {simulation.name}</h1> <h1>Simulation Detail: {simulation.name}</h1>
<div class="header-actions no-print"> <div class="header-actions no-print">
<button class="export-btn" onclick={handleExportPDF} <button class="export-btn" onclick={handleExportPDF}>Export PDF</button>
>Export PDF</button <button class="edit-btn" onclick={() => (isEditing = true)}>Edit</button
>
<button class="edit-btn" onclick={() => (isEditing = true)}
>Edit</button
>
<button class="delete-btn" onclick={handleDelete}>Delete</button
> >
<button class="delete-btn" onclick={handleDelete}>Delete</button>
</div> </div>
{:else} {:else}
<div class="edit-form"> <div class="edit-form">
@@ -382,35 +365,23 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="sim-files">Upload Media:</label> <label for="sim-files">Upload Media:</label>
<input <input id="sim-files" type="file" multiple bind:files={uploadFiles} />
id="sim-files"
type="file"
multiple
bind:files={uploadFiles}
/>
</div> </div>
{#if saveError} {#if saveError}
<p class="error">{saveError}</p> <p class="error">{saveError}</p>
{/if} {/if}
{#if isUploading} {#if isUploading}
<div class="progress-bar-container"> <div class="progress-bar-container">
<div <div class="progress-bar" style={`width: ${uploadProgress}%`}></div>
class="progress-bar"
style={`width: ${uploadProgress}%`}
></div>
<span class="progress-text">{uploadProgress}%</span> <span class="progress-text">{uploadProgress}%</span>
</div> </div>
{/if} {/if}
<div class="form-actions"> <div class="form-actions">
<button <button class="save-btn" onclick={handleSave} disabled={isUploading}
class="save-btn" >Save</button
onclick={handleSave}
disabled={isUploading}>Save</button
> >
<button <button class="cancel-btn" onclick={cancelEdit} disabled={isUploading}
class="cancel-btn" >Cancel</button
onclick={cancelEdit}
disabled={isUploading}>Cancel</button
> >
</div> </div>
</div> </div>
@@ -430,6 +401,8 @@
? "Lawnmower" ? "Lawnmower"
: parsedConfig?.find((c) => c.key === "search.levy.max_steps") : parsedConfig?.find((c) => c.key === "search.levy.max_steps")
? "Levy" ? "Levy"
: parsedConfig?.find((c) => c.key.startsWith("search.sar"))
? "SAR"
: "Unknown"}</span : "Unknown"}</span
> >
</p> </p>
@@ -474,19 +447,11 @@
<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
>Value</th
></tr
>
</thead> </thead>
<tbody> <tbody>
{#each groups["_general"] as item} {#each groups["_general"] as item}
<tr <tr><td>{item.key}</td><td>{item.value}</td></tr>
><td>{item.key}</td><td
>{item.value}</td
></tr
>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -494,27 +459,16 @@
{/if} {/if}
{#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]} {#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]}
<div class="table-wrapper"> <div class="table-wrapper">
<h3 <h3 class="table-label" style="text-transform: capitalize;">
class="table-label"
style="text-transform: capitalize;"
>
{groupName} {groupName}
</h3> </h3>
<table class="config-table"> <table class="config-table">
<thead> <thead>
<tr <tr><th>Parameter</th><th>Value</th></tr>
><th>Parameter</th><th
>Value</th
></tr
>
</thead> </thead>
<tbody> <tbody>
{#each items as item} {#each items as item}
<tr <tr><td>{item.key}</td><td>{item.value}</td></tr>
><td>{item.key}</td><td
>{item.value}</td
></tr
>
{/each} {/each}
</tbody> </tbody>
</table> </table>

View File

@@ -104,14 +104,12 @@
<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" ? "Lawnmower"
: parsedConfig?.find( : parsedConfig?.find((c) => c.key === "search.levy.max_steps")
(c) => c.key === "search.levy.max_steps",
)
? "Levy" ? "Levy"
: parsedConfig?.find((c) => c.key.startsWith("search.sar"))
? "SAR"
: "Unknown"} : "Unknown"}
</span> </span>
</p> </p>
@@ -156,19 +154,11 @@
<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
>Value</th
></tr
>
</thead> </thead>
<tbody> <tbody>
{#each groups["_general"] as item} {#each groups["_general"] as item}
<tr <tr><td>{item.key}</td><td>{item.value}</td></tr>
><td>{item.key}</td><td
>{item.value}</td
></tr
>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -176,27 +166,16 @@
{/if} {/if}
{#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]} {#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]}
<div class="table-wrapper"> <div class="table-wrapper">
<h3 <h3 class="table-label" style="text-transform: capitalize;">
class="table-label"
style="text-transform: capitalize;"
>
{groupName} {groupName}
</h3> </h3>
<table class="config-table"> <table class="config-table">
<thead> <thead>
<tr <tr><th>Parameter</th><th>Value</th></tr>
><th>Parameter</th><th
>Value</th
></tr
>
</thead> </thead>
<tbody> <tbody>
{#each items as item} {#each items as item}
<tr <tr><td>{item.key}</td><td>{item.value}</td></tr>
><td>{item.key}</td><td
>{item.value}</td
></tr
>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -221,10 +200,7 @@
<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)}
alt={image}
/>
<p class="caption">{image}</p> <p class="caption">{image}</p>
</div> </div>
{/each} {/each}