// 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,`; } 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 ? `
${h3Cell}${lat}, ${lon}${r.latency_ms.toFixed(0)} ms