472 lines
13 KiB
Svelte
472 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
|
|
let simulations: Array<{
|
|
id: number;
|
|
name: string;
|
|
created_at: string;
|
|
config: string;
|
|
}> = $state([]);
|
|
let error = $state("");
|
|
|
|
let searchQuery = $state("");
|
|
let filterDateFrom = $state("");
|
|
let filterDateTo = $state("");
|
|
let filterAltMin = $state("");
|
|
let filterAltMax = $state("");
|
|
let filterUavMin = $state("");
|
|
let filterUavMax = $state("");
|
|
let filterUgvMin = $state("");
|
|
let filterUgvMax = $state("");
|
|
let filterPattern = $state("any");
|
|
|
|
let sortOrder = $state("newest");
|
|
let currentPage = $state(1);
|
|
let itemsPerPage = 10;
|
|
|
|
function parseConfig(config: string) {
|
|
if (!config)
|
|
return {
|
|
altitude: null,
|
|
uavMin: null,
|
|
uavMax: null,
|
|
ugvMin: null,
|
|
ugvMax: null,
|
|
};
|
|
let alt = config.match(/altitude:\s*([\d\.]+)/);
|
|
let uavMin = config.match(/## UAV[\s\S]*?min_mph:\s*([\d\.]+)/);
|
|
let uavMax = config.match(/## UAV[\s\S]*?max_mph:\s*([\d\.]+)/);
|
|
let ugvMin = config.match(/## UGV[\s\S]*?min_mph:\s*([\d\.]+)/);
|
|
let ugvMax = config.match(/## UGV[\s\S]*?max_mph:\s*([\d\.]+)/);
|
|
let pattern = config.match(/(spiral|lawnmower|levy):/);
|
|
return {
|
|
altitude: alt ? parseFloat(alt[1]) : null,
|
|
uavMin: uavMin ? parseFloat(uavMin[1]) : null,
|
|
uavMax: uavMax ? parseFloat(uavMax[1]) : null,
|
|
ugvMin: ugvMin ? parseFloat(ugvMin[1]) : null,
|
|
ugvMax: ugvMax ? parseFloat(ugvMax[1]) : null,
|
|
pattern: pattern ? pattern[1] : null,
|
|
};
|
|
}
|
|
|
|
let filteredSimulations = $derived(
|
|
simulations.filter((sim) => {
|
|
let matchText = sim.id.toString().includes(searchQuery);
|
|
if (!matchText) return false;
|
|
|
|
let simDate = new Date(sim.created_at).getTime();
|
|
if (filterDateFrom && simDate < new Date(filterDateFrom).getTime())
|
|
return false;
|
|
if (
|
|
filterDateTo &&
|
|
simDate > new Date(filterDateTo).getTime() + 86400000
|
|
)
|
|
return false;
|
|
|
|
let p = parseConfig(sim.config);
|
|
if (
|
|
filterAltMin &&
|
|
p.altitude !== null &&
|
|
p.altitude < parseFloat(filterAltMin)
|
|
)
|
|
return false;
|
|
if (
|
|
filterAltMax &&
|
|
p.altitude !== null &&
|
|
p.altitude > parseFloat(filterAltMax)
|
|
)
|
|
return false;
|
|
if (
|
|
filterUavMin &&
|
|
p.uavMin !== null &&
|
|
p.uavMin < parseFloat(filterUavMin)
|
|
)
|
|
return false;
|
|
if (
|
|
filterUavMax &&
|
|
p.uavMax !== null &&
|
|
p.uavMax > parseFloat(filterUavMax)
|
|
)
|
|
return false;
|
|
if (
|
|
filterUgvMin &&
|
|
p.ugvMin !== null &&
|
|
p.ugvMin < parseFloat(filterUgvMin)
|
|
)
|
|
return false;
|
|
if (
|
|
filterUgvMax &&
|
|
p.ugvMax !== null &&
|
|
p.ugvMax > parseFloat(filterUgvMax)
|
|
)
|
|
return false;
|
|
|
|
if (filterPattern !== "any" && p.pattern !== filterPattern)
|
|
return false;
|
|
|
|
return true;
|
|
}),
|
|
);
|
|
|
|
let sortedSimulations = $derived(
|
|
[...filteredSimulations].sort((a, b) => {
|
|
if (sortOrder === "newest") {
|
|
return (
|
|
new Date(b.created_at).getTime() -
|
|
new Date(a.created_at).getTime()
|
|
);
|
|
} else if (sortOrder === "oldest") {
|
|
return (
|
|
new Date(a.created_at).getTime() -
|
|
new Date(b.created_at).getTime()
|
|
);
|
|
} else if (sortOrder === "fastest" || sortOrder === "slowest") {
|
|
let pA = parseConfig(a.config);
|
|
let pB = parseConfig(b.config);
|
|
let speedA = (pA.uavMax || 0) + (pA.ugvMax || 0);
|
|
let speedB = (pB.uavMax || 0) + (pB.ugvMax || 0);
|
|
if (sortOrder === "fastest") {
|
|
return speedB - speedA;
|
|
} else {
|
|
return speedA - speedB;
|
|
}
|
|
}
|
|
return 0;
|
|
}),
|
|
);
|
|
|
|
let totalPages = $derived(
|
|
Math.max(1, Math.ceil(sortedSimulations.length / itemsPerPage)),
|
|
);
|
|
|
|
let paginatedSimulations = $derived(
|
|
sortedSimulations.slice(
|
|
(currentPage - 1) * itemsPerPage,
|
|
currentPage * itemsPerPage,
|
|
),
|
|
);
|
|
|
|
function goToPage(page: number) {
|
|
if (page >= 1 && page <= totalPages) {
|
|
currentPage = page;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (
|
|
searchQuery ||
|
|
sortOrder ||
|
|
filterDateFrom ||
|
|
filterDateTo ||
|
|
filterAltMin ||
|
|
filterAltMax ||
|
|
filterUavMin ||
|
|
filterUavMax ||
|
|
filterUgvMin ||
|
|
filterUgvMax ||
|
|
filterPattern
|
|
) {
|
|
currentPage = 1;
|
|
}
|
|
});
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const res = await fetch("/api/simulations");
|
|
if (!res.ok) throw new Error("Failed to fetch simulations");
|
|
simulations = await res.json();
|
|
} catch (e: any) {
|
|
error = e.message;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<h1>Sim-Link Simulation Results</h1>
|
|
|
|
{#if error}
|
|
<p class="error">Error: {error}</p>
|
|
{/if}
|
|
|
|
<div class="page-layout">
|
|
<aside class="sidebar">
|
|
<details open class="filter-group">
|
|
<summary>Search & Sort</summary>
|
|
<div class="filter-content">
|
|
<input
|
|
type="text"
|
|
placeholder="Search ID..."
|
|
bind:value={searchQuery}
|
|
class="input-field full-width"
|
|
/>
|
|
<select bind:value={sortOrder} class="input-field full-width">
|
|
<option value="newest">Newest First</option>
|
|
<option value="oldest">Oldest First</option>
|
|
<option value="fastest">Fastest</option>
|
|
<option value="slowest">Slowest</option>
|
|
</select>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="filter-group">
|
|
<summary>Algorithm Pattern</summary>
|
|
<div class="filter-content">
|
|
<select
|
|
bind:value={filterPattern}
|
|
class="input-field full-width"
|
|
>
|
|
<option value="any">Any</option>
|
|
<option value="spiral">Spiral</option>
|
|
<option value="lawnmower">Lawnmower</option>
|
|
<option value="levy">Levy</option>
|
|
</select>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="filter-group">
|
|
<summary>Date Range</summary>
|
|
<div class="filter-content col">
|
|
<label
|
|
>From <input
|
|
type="date"
|
|
bind:value={filterDateFrom}
|
|
class="input-field"
|
|
/></label
|
|
>
|
|
<label
|
|
>To <input
|
|
type="date"
|
|
bind:value={filterDateTo}
|
|
class="input-field"
|
|
/></label
|
|
>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="filter-group">
|
|
<summary>Flight Altitude</summary>
|
|
<div class="filter-content col">
|
|
<label
|
|
>Min (ft) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterAltMin}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
<label
|
|
>Max (ft) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterAltMax}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="filter-group">
|
|
<summary>UAV Speed</summary>
|
|
<div class="filter-content col">
|
|
<label
|
|
>Min (mph) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterUavMin}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
<label
|
|
>Max (mph) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterUavMax}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="filter-group">
|
|
<summary>UGV Speed</summary>
|
|
<div class="filter-content col">
|
|
<label
|
|
>Min (mph) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterUgvMin}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
<label
|
|
>Max (mph) <input
|
|
type="number"
|
|
step="0.1"
|
|
bind:value={filterUgvMax}
|
|
class="input-field small-num"
|
|
/></label
|
|
>
|
|
</div>
|
|
</details>
|
|
</aside>
|
|
|
|
<main class="table-content">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Sim Name</th>
|
|
<th>Date Run</th>
|
|
<th>Link</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each paginatedSimulations as sim}
|
|
<tr>
|
|
<td>{sim.id}</td>
|
|
<td>{sim.name}</td>
|
|
<td>{sim.created_at}</td>
|
|
<td
|
|
><a href={`/simulation/${sim.id}`}>View Details</a
|
|
></td
|
|
>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="pagination">
|
|
<button
|
|
onclick={() => goToPage(currentPage - 1)}
|
|
disabled={currentPage === 1}
|
|
>
|
|
Previous
|
|
</button>
|
|
<span>Page {currentPage} of {totalPages}</span>
|
|
<button
|
|
onclick={() => goToPage(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<style>
|
|
:global(body) {
|
|
font-family: "JetBrains Mono", Courier, monospace;
|
|
background-color: #ffffff;
|
|
color: #000000;
|
|
}
|
|
.page-layout {
|
|
display: flex;
|
|
gap: 30px;
|
|
align-items: flex-start;
|
|
}
|
|
.sidebar {
|
|
width: 300px;
|
|
flex-shrink: 0;
|
|
}
|
|
.table-content {
|
|
flex-grow: 1;
|
|
overflow-x: auto;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin-bottom: 20px;
|
|
}
|
|
th,
|
|
td {
|
|
border: 1px solid #000000;
|
|
padding: 12px;
|
|
text-align: left;
|
|
}
|
|
th {
|
|
background-color: #e0e0e0;
|
|
}
|
|
tbody tr:nth-child(even) {
|
|
background-color: #f9f9f9;
|
|
}
|
|
h1 {
|
|
font-size: 28px;
|
|
margin-bottom: 30px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #000;
|
|
}
|
|
a {
|
|
color: #0000ff;
|
|
text-decoration: underline;
|
|
}
|
|
a:visited {
|
|
color: #800080;
|
|
}
|
|
.error {
|
|
color: red;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
}
|
|
.filter-group {
|
|
border: 1px solid #000;
|
|
margin-bottom: 10px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
summary {
|
|
font-weight: bold;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
background-color: #e0e0e0;
|
|
border-bottom: 1px solid #000;
|
|
}
|
|
details:not([open]) summary {
|
|
border-bottom: none;
|
|
}
|
|
.filter-content {
|
|
padding: 15px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
.filter-content.col {
|
|
gap: 10px;
|
|
}
|
|
.full-width {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
.input-field {
|
|
padding: 5px;
|
|
border: 1px solid #000;
|
|
font-family: inherit;
|
|
}
|
|
.small-num {
|
|
width: 70px;
|
|
}
|
|
.pagination {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
button {
|
|
padding: 5px 10px;
|
|
border: 1px solid #000;
|
|
background-color: #e0e0e0;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
button:disabled {
|
|
background-color: #f0f0f0;
|
|
color: #888;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|