// 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,GeoTIFF`; } 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 © Esri — 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 = ` EXECUTING RETRIEVAL...`; timingFeed.innerHTML = ` 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 = ` Error in search`; }) .finally(() => { searchBtn.disabled = false; searchBtn.innerHTML = ` 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 = ` 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 ? `
` : ''; card.innerHTML = `
${item.class} S: ${score} ${item.modality} ${actionHTML}

${item.class}

#0${rank}
Lat: ${lat} Lon: ${lon} Dist: ${dist} H3: ${h3Cell}
`; 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 = ` RGB`; imgEl.src = `/api/render-bands?path=${encodeURIComponent(path)}&bands=FCC`; } else { btn.setAttribute("data-active", "rgb"); btn.innerHTML = ` 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 = ` 0${rank} ${item.class} ${item.modality.toUpperCase()} ${h3Cell} ${lat}, ${lon} ${dist} ${score} `; 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(`
Rank #${rank} • ${item.class}
Modality: ${item.modality}
Score: ${score}
Dist: ${dist}
`); 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 = ` ${r.model} ${r.same_r1.toFixed(3)} ${r.same_r5.toFixed(3)} ${r.same_r10.toFixed(3)} ${r.cross_r1.toFixed(3)} ${r.cross_r5.toFixed(3)} ${r.cross_r10.toFixed(3)} ${r.latency_ms.toFixed(0)} ms `; 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(); } });