// src/plugins/satellite-predictions.ts var predWindow = window; var dom = { page: document.getElementById("tab-satellites"), filter: document.getElementById("sat-pred-filter"), minElevation: document.getElementById("sat-pred-min-el"), category: document.getElementById("sat-pred-category"), currentList: document.getElementById("sat-pred-current-list"), upcomingList: document.getElementById("sat-pred-list"), currentSection: document.getElementById("sat-pred-current-section"), upcomingSection: document.getElementById("sat-pred-upcoming-section"), status: document.getElementById("sat-pred-status") }; var PAGE_SIZE = 50; var predShowAll = false; var predData = []; var predFilterText = ""; var predMinEl = 0; var predCategory = "all"; var predSatCount = 0; var predCountdownTimer = null; function isPageVisible() { return !!dom.page && dom.page.style.display !== "none"; } function azToCardinal(deg) { const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; return dirs[Math.round(deg / 45) % 8] ?? "N"; } function formatPredTime(ms) { const d = new Date(ms); const now = /* @__PURE__ */ new Date(); const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : ""; const hh = String(d.getUTCHours()).padStart(2, "0"); const mm = String(d.getUTCMinutes()).padStart(2, "0"); return `${day}${hh}:${mm}`; } function formatPredDuration(s) { if (s >= 60) return `${Math.round(s / 60)} min`; return `${s}s`; } function formatCountdown(ms) { const totalSec = Math.max(0, Math.floor(ms / 1e3)); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return `${m}:${String(s).padStart(2, "0")}`; } function elevationClass(deg) { if (deg >= 45) return "sat-pred-el-high"; if (deg >= 10) return "sat-pred-el-mid"; return "sat-pred-el-low"; } function stopCountdownTimer() { if (predCountdownTimer) { clearInterval(predCountdownTimer); predCountdownTimer = null; } } function startCountdownTimer(container) { const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? []; if (countdownEls.length === 0) return; predCountdownTimer = setInterval(() => { if (!isPageVisible()) { stopCountdownTimer(); return; } const n = Date.now(); let anyActive = false; for (const el of countdownEls) { const los = Number.parseInt(el.dataset.los ?? "0", 10); const rem = los - n; if (rem > 0) { el.textContent = formatCountdown(rem); anyActive = true; } else { el.textContent = "0:00"; } } if (!anyActive) { stopCountdownTimer(); render(filtered()); } }, 1e3); } function buildCurrentPassRow(pass, now) { const row = document.createElement("div"); row.className = "sat-pred-row-current"; const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`; const remaining = Math.max(0, pass.los_ms - now); row.innerHTML = [ `${pass.satellite}`, `${pass.max_elevation_deg.toFixed(1)}°`, `${formatPredTime(pass.aos_ms)}`, `${formatPredTime(pass.los_ms)}`, `${formatCountdown(remaining)}`, `${dir}` ].join(""); return row; } function buildUpcomingPassRow(pass) { const row = document.createElement("div"); row.className = "sat-pred-row"; const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`; row.innerHTML = [ `${formatPredTime(pass.aos_ms)}`, `${pass.satellite}`, `${pass.max_elevation_deg.toFixed(1)}°`, `${formatPredDuration(pass.duration_s)}`, `${dir}` ].join(""); return row; } function filtered() { let items = predData; if (predCategory !== "all") items = items.filter((p) => p.category === predCategory); if (predMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= predMinEl); if (predFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(predFilterText)); return items; } function applyFilters() { render(filtered()); } var filterInput = dom.filter; filterInput?.addEventListener("input", () => { predFilterText = filterInput.value.trim().toUpperCase(); applyFilters(); }); var minElevationSelect = dom.minElevation; minElevationSelect?.addEventListener("change", () => { predMinEl = Number.parseInt(minElevationSelect.value, 10) || 0; applyFilters(); }); var categorySelect = dom.category; categorySelect?.addEventListener("change", () => { predCategory = categorySelect.value; applyFilters(); }); function render(passes, error) { stopCountdownTimer(); if (error) { if (dom.currentList) dom.currentList.innerHTML = ""; if (dom.upcomingList) dom.upcomingList.innerHTML = ""; if (dom.currentSection) dom.currentSection.style.display = "none"; if (dom.upcomingSection) dom.upcomingSection.style.display = "none"; if (dom.status) dom.status.textContent = error; return; } if (!Array.isArray(passes) || passes.length === 0) { if (dom.currentList) dom.currentList.innerHTML = ""; if (dom.upcomingList) dom.upcomingList.innerHTML = ""; if (dom.currentSection) dom.currentSection.style.display = "none"; if (dom.upcomingSection) dom.upcomingSection.style.display = "none"; if (dom.status) dom.status.textContent = "No passes found in the next 24 hours."; return; } const now = Date.now(); const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now); const upcoming = passes.filter((p) => p.aos_ms > now); if (dom.currentSection) dom.currentSection.style.display = current.length > 0 ? "" : "none"; if (dom.currentList) { if (current.length === 0) { dom.currentList.innerHTML = ""; } else { const frag = document.createDocumentFragment(); for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now)); dom.currentList.replaceChildren(frag); } } const upcomingLimit = predShowAll ? upcoming.length : PAGE_SIZE; const visibleUpcoming = upcoming.slice(0, upcomingLimit); const hiddenCount = upcoming.length - visibleUpcoming.length; if (dom.upcomingSection) dom.upcomingSection.style.display = upcoming.length > 0 ? "" : "none"; if (dom.upcomingList) { const frag = document.createDocumentFragment(); for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass)); if (hiddenCount > 0) { const moreRow = document.createElement("div"); moreRow.className = "sat-pred-row"; moreRow.style.cursor = "pointer"; moreRow.style.textAlign = "center"; moreRow.innerHTML = `Show ${hiddenCount} more passes…`; moreRow.addEventListener("click", () => { predShowAll = true; render(filtered()); }); frag.appendChild(moreRow); } dom.upcomingList.replaceChildren(frag); } if (dom.status) { let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`; if (predSatCount > 0) text += ` · ${predSatCount} satellites tracked`; dom.status.textContent = text; } if (current.length > 0 && isPageVisible()) { startCountdownTimer(dom.currentList); } } async function load() { if (dom.status) dom.status.textContent = "Loading predictions…"; if (dom.currentList) dom.currentList.innerHTML = ""; if (dom.upcomingList) dom.upcomingList.innerHTML = ""; try { const resp = await fetch("/sat_passes"); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); predSatCount = data.satellite_count || 0; if (data.error) { predData = []; render([], data.error); } else { predData = data.passes || []; render(filtered()); } } catch (error) { render([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`); } } function clearPredictionDom() { stopCountdownTimer(); if (dom.currentList) dom.currentList.innerHTML = ""; if (dom.upcomingList) dom.upcomingList.innerHTML = ""; } predWindow.clearSatPredictionDom = clearPredictionDom; predWindow.refreshSatPredictions = function() { predShowAll = false; void load(); };