SatFetch / src /ui /static /app.js
karansharmaworkspace's picture
Upload 68 files
f343f06 verified
Raw
History Blame Contribute Delete
32.2 kB
// SatFetch Client Application Logic
document.addEventListener("DOMContentLoaded", () => {
// -------------------------------------------------------------------------
// View State Navigation (Landing -> Dashboard)
// -------------------------------------------------------------------------
const landingPage = document.getElementById("landing-page");
const terminalPage = document.getElementById("terminal-page");
const launchBtn = document.getElementById("launch-btn");
launchBtn.addEventListener("click", () => {
landingPage.classList.add("slide-out");
setTimeout(() => {
landingPage.style.display = "none";
terminalPage.classList.add("active");
// Trigger leaflet map resize after tab/container becomes visible
if (map) {
map.invalidateSize();
}
}, 600);
});
// -------------------------------------------------------------------------
// Tabs Navigation
// -------------------------------------------------------------------------
const navTabs = document.querySelectorAll(".nav-tab");
const tabContents = document.querySelectorAll(".tab-content");
navTabs.forEach(tab => {
tab.addEventListener("click", () => {
navTabs.forEach(t => t.classList.remove("active"));
tabContents.forEach(c => c.classList.remove("active"));
tab.classList.add("active");
const activeTabId = tab.getAttribute("data-tab");
document.getElementById(activeTabId).classList.add("active");
// Handle Leaflet refresh if search tab activated
if (activeTabId === "search-pane" && map) {
setTimeout(() => map.invalidateSize(), 50);
}
// Load benchmarks if benchmarks tab activated
if (activeTabId === "benchmarks-pane") {
loadBenchmarks();
}
});
});
// -------------------------------------------------------------------------
// Search Mode Toggle (Image vs Text)
// -------------------------------------------------------------------------
const modeImageBtn = document.getElementById("mode-image-btn");
const modeTextBtn = document.getElementById("mode-text-btn");
const imageUploadArea = document.getElementById("image-upload-area");
const textQueryArea = document.getElementById("text-query-area");
let activeSearchMode = "image"; // 'image' or 'text'
const retrievalLevelSelect = document.getElementById("retrieval-level");
const spatialOptionsGroup = document.getElementById("spatial-options-group");
function toggleSpatialOptions() {
if (retrievalLevelSelect.value === "level4") {
spatialOptionsGroup.style.display = "block";
} else {
spatialOptionsGroup.style.display = "none";
}
}
retrievalLevelSelect.addEventListener("change", toggleSpatialOptions);
toggleSpatialOptions();
modeImageBtn.addEventListener("click", () => {
modeImageBtn.classList.add("active");
modeTextBtn.classList.remove("active");
imageUploadArea.style.display = "block";
textQueryArea.style.display = "none";
activeSearchMode = "image";
});
modeTextBtn.addEventListener("click", () => {
modeTextBtn.classList.add("active");
modeImageBtn.classList.remove("active");
imageUploadArea.style.display = "none";
textQueryArea.style.display = "block";
activeSearchMode = "text";
});
// -------------------------------------------------------------------------
// File Upload Handler
// -------------------------------------------------------------------------
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
const uploadPreview = document.getElementById("upload-preview");
const previewImg = document.getElementById("preview-img");
const previewFilename = document.getElementById("preview-filename");
const previewSize = document.getElementById("preview-size");
const clearUpload = document.getElementById("clear-upload");
let uploadedFile = null;
dropZone.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.style.borderColor = "var(--primary-color)";
dropZone.style.backgroundColor = "var(--primary-glow)";
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.borderColor = "var(--border-color)";
dropZone.style.backgroundColor = "transparent";
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.style.borderColor = "var(--border-color)";
dropZone.style.backgroundColor = "transparent";
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener("change", (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
clearUpload.addEventListener("click", (e) => {
e.stopPropagation();
uploadedFile = null;
fileInput.value = "";
previewImg.src = "";
uploadPreview.style.display = "none";
dropZone.style.display = "block";
});
function handleFile(file) {
uploadedFile = file;
previewFilename.textContent = file.name;
// Format size
const sizeMB = (file.size / (1024 * 1024)).toFixed(1);
previewSize.textContent = `${sizeMB} MB`;
// Check if file is TIFF (cannot display TIFF natively in most browsers)
const isTiff = file.name.endsWith(".tif") || file.name.endsWith(".tiff");
if (isTiff) {
// Beautiful local SVG placeholder for TIFF files (offline safe)
previewImg.src = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><rect width="100" height="100" rx="10" fill="%23f8fafc" stroke="%23cbd5e1" stroke-width="2"/><path d="M35 25 H55 L65 35 V75 H35 Z" fill="white" stroke="%233b82f6" stroke-width="2"/><path d="M55 25 V35 H65" fill="none" stroke="%233b82f6" stroke-width="2"/><text x="50" y="60" font-family="system-ui, sans-serif" font-size="9" font-weight="bold" fill="%231e293b" text-anchor="middle">GeoTIFF</text></svg>`;
} else {
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result;
};
reader.readAsDataURL(file);
}
dropZone.style.display = "none";
uploadPreview.style.display = "block";
}
// -------------------------------------------------------------------------
// Leaflet Map Integration
// -------------------------------------------------------------------------
let map;
let centerMarker;
let searchRadiusCircle;
let resultsMarkersGroup = L.layerGroup();
let h3GridGroup = L.layerGroup();
const indiaCenter = [20.5937, 78.9629];
function initMap() {
const mapEl = document.getElementById("map");
if (!mapEl) return;
map = L.map("map").setView(indiaCenter, 5);
// Light-themed GIS Tile Layer (Esri World Topo Map)
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Sources: Esri, DeLorme, NAVTEQ, USGS, NOAA, and the GIS User Community'
}).addTo(map);
// Set pointer marker
centerMarker = L.marker(indiaCenter, { draggable: true }).addTo(map);
// Draw initial radius
updateMapCircle(indiaCenter, parseInt(document.getElementById("radius-input").value));
// Sync coordinate boxes on marker drag
centerMarker.on("dragend", (e) => {
const latlng = e.target.getLatLng();
document.getElementById("lat-input").value = latlng.lat.toFixed(5);
document.getElementById("lon-input").value = latlng.lng.toFixed(5);
updateMapCircle([latlng.lat, latlng.lng], parseInt(document.getElementById("radius-input").value));
});
// Click on map to place coordinate
map.on("click", (e) => {
const latlng = e.latlng;
centerMarker.setLatLng(latlng);
document.getElementById("lat-input").value = latlng.lat.toFixed(5);
document.getElementById("lon-input").value = latlng.lng.toFixed(5);
updateMapCircle([latlng.lat, latlng.lng], parseInt(document.getElementById("radius-input").value));
});
resultsMarkersGroup.addTo(map);
h3GridGroup.addTo(map);
}
function updateMapCircle(coords, radiusKm) {
if (!map) return;
if (searchRadiusCircle) {
map.removeLayer(searchRadiusCircle);
}
searchRadiusCircle = L.circle(coords, {
radius: radiusKm * 1000,
color: "#2563eb",
fillColor: "#3b82f6",
fillOpacity: 0.1,
weight: 1.5
}).addTo(map);
}
// Connect slider inputs
const radiusInput = document.getElementById("radius-input");
const radiusVal = document.getElementById("radius-val");
radiusInput.addEventListener("input", (e) => {
const val = e.target.value;
radiusVal.textContent = `${val} km`;
if (map) {
const lat = parseFloat(document.getElementById("lat-input").value);
const lon = parseFloat(document.getElementById("lon-input").value);
updateMapCircle([lat, lon], parseInt(val));
}
});
const kInput = document.getElementById("k-input");
const kVal = document.getElementById("k-val");
kInput.addEventListener("input", (e) => {
kVal.textContent = e.target.value;
});
initMap();
// -------------------------------------------------------------------------
// Execution and API Requests
// -------------------------------------------------------------------------
const searchBtn = document.getElementById("search-btn");
const resultsEmpty = document.getElementById("results-empty");
const resultsGrid = document.getElementById("results-grid");
const resultsTableContainer = document.getElementById("results-table-container");
const resultsTableBody = document.getElementById("results-table-body");
const timingFeed = document.getElementById("timing-feed");
searchBtn.addEventListener("click", () => {
if (activeSearchMode === "image" && !uploadedFile) {
alert("Please upload a query satellite image first.");
return;
}
if (activeSearchMode === "text" && !document.getElementById("text-query-input").value.trim()) {
alert("Please enter a text search query.");
return;
}
searchBtn.disabled = true;
searchBtn.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> EXECUTING RETRIEVAL...`;
timingFeed.innerHTML = `<i class="fa-solid fa-satellite fa-spin"></i> Aligning sensor domains...`;
const formData = new FormData();
const level = document.getElementById("retrieval-level").value;
const k = document.getElementById("k-input").value;
const modality = document.getElementById("query-modality").value;
let url = "/api/search";
formData.append("k", k);
formData.append("level", level);
formData.append("query_modality", modality);
if (level === "level4") {
const lat = document.getElementById("lat-input").value;
const lon = document.getElementById("lon-input").value;
const radius = document.getElementById("radius-input").value;
formData.append("lat", lat);
formData.append("lon", lon);
formData.append("radius_km", radius);
}
if (activeSearchMode === "image") {
formData.append("file", uploadedFile);
} else {
url = "/api/search-text";
formData.append("text_query", document.getElementById("text-query-input").value);
}
fetch(url, {
method: "POST",
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error("Server error occurred during retrieval.");
}
return response.json();
})
.then(data => {
renderResults(data);
})
.catch(err => {
console.error(err);
alert(`Search failed: ${err.message}`);
timingFeed.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="color:var(--error)"></i> Error in search`;
})
.finally(() => {
searchBtn.disabled = false;
searchBtn.innerHTML = `<i class="fa-solid fa-circle-play"></i> RUN ALIGNMENT & SEARCH`;
});
});
// -------------------------------------------------------------------------
// Results Rendering & Leaflet Marker Plotting
// -------------------------------------------------------------------------
function renderResults(data) {
const results = data.results;
const timeMs = data.query_time_ms.toFixed(0);
const device = data.device || "cpu";
// Update telemetry
document.getElementById("telemetry-device").textContent = device.toUpperCase();
timingFeed.innerHTML = `<i class="fa-solid fa-bolt"></i> Completed in ${timeMs}ms`;
// Clear existing markers & overlays
resultsMarkersGroup.clearLayers();
h3GridGroup.clearLayers();
if (results.length === 0) {
resultsEmpty.style.display = "flex";
resultsGrid.style.display = "none";
resultsTableContainer.style.display = "none";
return;
}
resultsEmpty.style.display = "none";
resultsGrid.style.display = "grid";
resultsTableContainer.style.display = "block";
resultsGrid.innerHTML = "";
resultsTableBody.innerHTML = "";
const mapBounds = L.latLngBounds();
results.forEach((item, index) => {
const rank = index + 1;
const score = item.score.toFixed(4);
const lat = item.lat ? item.lat.toFixed(5) : "N/A";
const lon = item.lon ? item.lon.toFixed(5) : "N/A";
const dist = item.distance_km ? `${item.distance_km.toFixed(1)} km` : "N/A";
const h3Cell = item.h3_cell || "N/A";
// Render Card
const card = document.createElement("div");
card.className = "result-card card";
// True color vs FCC render toggle for MS
const isMs = item.modality === "multispectral";
const actionHTML = isMs ? `
<div class="result-actions">
<button class="action-btn toggle-fcc-btn" data-path="${item.original_path}" data-active="rgb" title="Toggle NIR False Color Composite (FCC)">
<i class="fa-solid fa-wand-magic-sparkles"></i> FCC
</button>
<button class="action-btn plot-spectral-btn" data-path="${item.original_path}" title="Plot Spectral Signature">
<i class="fa-solid fa-chart-line"></i>
</button>
</div>
` : '';
card.innerHTML = `
<div class="result-img-wrapper">
<img src="${item.gallery_path}" alt="${item.class}" id="img-result-${index}">
<span class="result-score-badge">S: ${score}</span>
<span class="result-modality-tag">${item.modality}</span>
${actionHTML}
</div>
<div class="result-content">
<div class="result-meta-title">
<h4>${item.class}</h4>
<span class="result-meta-rank">#0${rank}</span>
</div>
<div class="result-details">
<span><strong>Lat:</strong> ${lat}</span>
<span><strong>Lon:</strong> ${lon}</span>
<span><strong>Dist:</strong> ${dist}</span>
<span><strong>H3:</strong> ${h3Cell}</span>
</div>
</div>
`;
resultsGrid.appendChild(card);
// Add FCC toggle listener
if (isMs) {
const btn = card.querySelector(".toggle-fcc-btn");
btn.addEventListener("click", (e) => {
e.stopPropagation();
const activeMode = btn.getAttribute("data-active");
const path = btn.getAttribute("data-path");
const imgEl = document.getElementById(`img-result-${index}`);
if (activeMode === "rgb") {
btn.setAttribute("data-active", "fcc");
btn.innerHTML = `<i class="fa-solid fa-image"></i> RGB`;
imgEl.src = `/api/render-bands?path=${encodeURIComponent(path)}&bands=FCC`;
} else {
btn.setAttribute("data-active", "rgb");
btn.innerHTML = `<i class="fa-solid fa-wand-magic-sparkles"></i> FCC`;
imgEl.src = `/api/render-bands?path=${encodeURIComponent(path)}&bands=RGB`;
}
});
// Add spectral plot listener
const spectralBtn = card.querySelector(".plot-spectral-btn");
spectralBtn.addEventListener("click", () => {
openSpectralModal(item.original_path, item.class);
});
}
// Render Table Row
const row = document.createElement("tr");
row.innerHTML = `
<td><strong>0${rank}</strong></td>
<td><img src="${item.gallery_path}" class="table-thumbnail"></td>
<td><strong>${item.class}</strong></td>
<td><span class="badge">${item.modality.toUpperCase()}</span></td>
<td><code style="font-family:var(--font-mono)">${h3Cell}</code></td>
<td><code>${lat}, ${lon}</code></td>
<td>${dist}</td>
<td class="table-score">${score}</td>
`;
resultsTableBody.appendChild(row);
// Plot leaf-let marker
if (item.lat && item.lon) {
const pCoords = [item.lat, item.lon];
const marker = L.marker(pCoords).addTo(resultsMarkersGroup);
// Marker popup with thumbnail
marker.bindPopup(`
<div style="width:140px; text-align:center;">
<img src="${item.gallery_path}" style="width:100%; border-radius:4px; margin-bottom:0.375rem;">
<strong>Rank #${rank} &bull; ${item.class}</strong><br>
Modality: ${item.modality}<br>
Score: ${score}<br>
Dist: ${dist}
</div>
`);
mapBounds.extend(pCoords);
// If H3 cell boundary polygon coordinates are returned, draw the hexagon
if (item.h3_boundary) {
L.polygon(item.h3_boundary, {
color: "#10b981",
fillColor: "#10b981",
fillOpacity: 0.15,
weight: 1
}).addTo(h3GridGroup);
}
}
});
// Fit map bounds to show results
if (map && results.some(item => item.lat && item.lon)) {
map.fitBounds(mapBounds, { padding: [50, 50] });
}
}
// -------------------------------------------------------------------------
// Spectral Modal & Graphing Logic
// -------------------------------------------------------------------------
const modal = document.getElementById("spectral-modal");
const closeModalBtn = document.getElementById("close-modal-btn");
let spectralChartInstance = null;
closeModalBtn.addEventListener("click", () => {
modal.classList.remove("active");
});
function openSpectralModal(path, className) {
modal.classList.add("active");
document.getElementById("modal-title").textContent = `Sentinel-2 Spectral Signature - ${className}`;
fetch(`/api/spectral-signature?path=${encodeURIComponent(path)}`)
.then(res => res.json())
.then(data => {
renderSpectralChart(data.reflectance);
})
.catch(err => {
console.error(err);
alert("Failed to load spectral bands.");
});
}
function renderSpectralChart(values) {
const ctx = document.getElementById("spectral-chart").getContext("2d");
const bands = ["B01 (Aer)", "B02 (B)", "B03 (G)", "B04 (R)", "B05 (RE1)", "B06 (RE2)", "B07 (RE3)", "B08 (NIR)", "B08A (N2)", "B09 (WV)", "B10 (Cir)", "B11 (SW1)", "B12 (SW2)"];
if (spectralChartInstance) {
spectralChartInstance.destroy();
}
spectralChartInstance = new Chart(ctx, {
type: "line",
data: {
labels: bands,
datasets: [{
label: "Reflectance Index",
data: values,
borderColor: "#2563eb",
backgroundColor: "rgba(37, 99, 235, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: "Scaled Reflectance Value" }
}
}
}
});
}
// -------------------------------------------------------------------------
// System Benchmarks Tab Rendering
// -------------------------------------------------------------------------
let benchmarksLoaded = false;
let recallChartInstance = null;
let latencyChartInstance = null;
function loadBenchmarks() {
if (benchmarksLoaded) return;
fetch("/api/benchmarks")
.then(res => res.json())
.then(data => {
renderBenchmarksGrid(data);
benchmarksLoaded = true;
})
.catch(err => {
console.error("Failed to load benchmarks", err);
});
}
function renderBenchmarksGrid(results) {
// Render comparison table
const tbody = document.getElementById("benchmarks-table-body");
tbody.innerHTML = "";
results.forEach(r => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td><strong>${r.model}</strong></td>
<td>${r.same_r1.toFixed(3)}</td>
<td><strong>${r.same_r5.toFixed(3)}</strong></td>
<td>${r.same_r10.toFixed(3)}</td>
<td>${r.cross_r1.toFixed(3)}</td>
<td><strong>${r.cross_r5.toFixed(3)}</strong></td>
<td>${r.cross_r10.toFixed(3)}</td>
<td><code>${r.latency_ms.toFixed(0)} ms</code></td>
`;
tbody.appendChild(tr);
});
// Plot recall chart
const ctxRecall = document.getElementById("recall-chart").getContext("2d");
const labels = results.map(r => r.model);
const sameR5 = results.map(r => r.same_r5);
const crossR5 = results.map(r => r.cross_r5);
if (recallChartInstance) recallChartInstance.destroy();
recallChartInstance = new Chart(ctxRecall, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "Same-Modal Recall@5",
data: sameR5,
backgroundColor: "rgba(37, 99, 235, 0.7)",
borderColor: "#2563eb",
borderWidth: 1
},
{
label: "Cross-Modal Recall@5",
data: crossR5,
backgroundColor: "rgba(16, 185, 129, 0.7)",
borderColor: "#10b981",
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 0.6,
title: { display: true, text: "Score" }
}
}
}
});
// Plot latency chart
const ctxLatency = document.getElementById("latency-chart").getContext("2d");
const latencies = results.map(r => r.latency_ms);
if (latencyChartInstance) latencyChartInstance.destroy();
latencyChartInstance = new Chart(ctxLatency, {
type: "bar",
data: {
labels: labels,
datasets: [{
label: "Average Search Latency (ms)",
data: latencies,
backgroundColor: "rgba(245, 158, 11, 0.7)",
borderColor: "#f59e0b",
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: "Latency (ms)" }
}
}
}
});
}
// -------------------------------------------------------------------------
// Sandbox Queries Click Handler
// -------------------------------------------------------------------------
document.querySelectorAll(".sample-query-tile").forEach(tile => {
tile.addEventListener("click", async () => {
const path = tile.getAttribute("data-path");
const name = tile.getAttribute("data-name");
const type = tile.getAttribute("data-type");
// Visually toggle active loading state
tile.style.borderColor = "var(--primary-color)";
try {
// Fetch the image as a Blob and create a File object
const res = await fetch(path);
const blob = await res.blob();
const file = new File([blob], name, { type: blob.type || "image/png" });
// Set search mode to image query
document.getElementById("mode-image-btn").click();
// Select target query modality dropdown
document.getElementById("query-modality").value = type;
// Load file to preview
handleFile(file);
// Automatically trigger search click
searchBtn.click();
} catch (err) {
console.error("Failed to load sample query file:", err);
alert("Failed to load sample query file: " + err.message);
} finally {
setTimeout(() => {
tile.style.borderColor = "#ced4da";
}, 1000);
}
});
});
// -------------------------------------------------------------------------
// Rotating Header Banner
// -------------------------------------------------------------------------
const bannerMsgs = document.querySelectorAll(".banner-msg");
if (bannerMsgs.length > 0) {
let currentMsgIndex = 0;
setInterval(() => {
bannerMsgs[currentMsgIndex].classList.remove("active");
currentMsgIndex = (currentMsgIndex + 1) % bannerMsgs.length;
bannerMsgs[currentMsgIndex].classList.add("active");
}, 5000); // rotates every 5 seconds
}
// -------------------------------------------------------------------------
// Interactive LinkedIn Profile Links
// -------------------------------------------------------------------------
const members = ["ayush", "karan", "anurag"];
members.forEach(member => {
const linkEl = document.getElementById(`${member}-linkedin-link`);
const editEl = document.getElementById(`${member}-edit-linkedin`);
const inputEl = document.getElementById(`${member}-linkedin-input`);
function showLink(url) {
linkEl.href = url;
linkEl.style.display = "inline-block";
editEl.style.display = "inline-block";
inputEl.style.display = "none";
}
function showInput() {
linkEl.style.display = "none";
editEl.style.display = "none";
inputEl.style.display = "inline-block";
inputEl.value = localStorage.getItem(`${member}_linkedin_url`) || "";
}
const savedUrl = localStorage.getItem(`${member}_linkedin_url`);
if (savedUrl) {
showLink(savedUrl);
} else {
showInput();
}
inputEl.addEventListener("change", (e) => {
let url = e.target.value.trim();
if (url) {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
localStorage.setItem(`${member}_linkedin_url`, url);
showLink(url);
}
});
editEl.addEventListener("click", (e) => {
e.preventDefault();
showInput();
});
});
// -------------------------------------------------------------------------
// Benchmarks Simulator Logic
// -------------------------------------------------------------------------
const simWeight = document.getElementById("sim-modality-weight");
const simNoise = document.getElementById("sim-noise-level");
const simH3Res = document.getElementById("sim-h3-resolution");
function updateSimulation() {
if (!simWeight) return;
const w = parseFloat(simWeight.value);
const n = parseFloat(simNoise.value);
const res = parseInt(simH3Res.value);
document.getElementById("sim-weight-val").innerText = w.toFixed(2);
document.getElementById("sim-noise-val").innerText = n.toFixed(2);
// Simulated precision recall curves formulas based on ZS-MC characteristics
const baseR1 = 0.45 + 0.12 * Math.sin(w * Math.PI) - 0.3 * n;
const r1 = Math.max(0.1, Math.min(0.99, baseR1));
const r5 = Math.max(r1 + 0.05, Math.min(0.99, r1 * 1.15 + 0.05));
const mapVal = Math.max(r5 + 0.05, Math.min(0.99, r5 * 1.12 + 0.04));
// Latency depends on H3 resolution level
const latency = Math.round(28 + (8 - res) * 4 + w * 2 + n * 2);
document.getElementById("sim-metric-r1").innerText = (r1 * 100).toFixed(1) + "%";
document.getElementById("sim-metric-r5").innerText = (r5 * 100).toFixed(1) + "%";
document.getElementById("sim-metric-map").innerText = (mapVal * 100).toFixed(1) + "%";
document.getElementById("sim-metric-latency").innerText = latency + " ms";
}
if (simWeight) {
simWeight.addEventListener("input", updateSimulation);
simNoise.addEventListener("input", updateSimulation);
simH3Res.addEventListener("change", updateSimulation);
updateSimulation();
}
});