// 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();
};