refactor: convert satellite view to TypeScript
This commit is contained in:
@@ -10,7 +10,7 @@ const pluginGroups = {
|
|||||||
};
|
};
|
||||||
const loaded = /* @__PURE__ */ new Set();
|
const loaded = /* @__PURE__ */ new Set();
|
||||||
const loading = /* @__PURE__ */ new Map();
|
const loading = /* @__PURE__ */ new Map();
|
||||||
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js"]);
|
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js"]);
|
||||||
function loadLegacyScript(path) {
|
function loadLegacyScript(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
|
|||||||
@@ -1,447 +1,472 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const satDom = {
|
(() => {
|
||||||
status: document.getElementById("sat-status"),
|
// src/plugins/sat.ts
|
||||||
liveView: document.getElementById("sat-live-view"),
|
var satWindow = window;
|
||||||
historyView: document.getElementById("sat-history-view"),
|
var satDom = {
|
||||||
predictionsView: document.getElementById("sat-predictions-view"),
|
status: document.getElementById("sat-status"),
|
||||||
liveLatest: document.getElementById("sat-live-latest"),
|
liveView: document.getElementById("sat-live-view"),
|
||||||
historyList: document.getElementById("sat-history-list"),
|
historyView: document.getElementById("sat-history-view"),
|
||||||
historyCount: document.getElementById("sat-history-count"),
|
predictionsView: document.getElementById("sat-predictions-view"),
|
||||||
filterInput: document.getElementById("sat-filter"),
|
liveLatest: document.getElementById("sat-live-latest"),
|
||||||
sortSelect: document.getElementById("sat-sort"),
|
historyList: document.getElementById("sat-history-list"),
|
||||||
typeFilter: document.getElementById("sat-type-filter"),
|
historyCount: document.getElementById("sat-history-count"),
|
||||||
lrptState: document.getElementById("sat-lrpt-state"),
|
filterInput: document.getElementById("sat-filter"),
|
||||||
viewLiveBtn: document.getElementById("sat-view-live"),
|
sortSelect: document.getElementById("sat-sort"),
|
||||||
viewHistoryBtn: document.getElementById("sat-view-history"),
|
typeFilter: document.getElementById("sat-type-filter"),
|
||||||
viewPredBtn: document.getElementById("sat-view-predictions"),
|
lrptState: document.getElementById("sat-lrpt-state"),
|
||||||
predFilter: document.getElementById("sat-pred-filter"),
|
viewLiveBtn: document.getElementById("sat-view-live"),
|
||||||
predMinEl: document.getElementById("sat-pred-min-el"),
|
viewHistoryBtn: document.getElementById("sat-view-history"),
|
||||||
predCategory: document.getElementById("sat-pred-category"),
|
viewPredBtn: document.getElementById("sat-view-predictions"),
|
||||||
predCurrentList: document.getElementById("sat-pred-current-list"),
|
predFilter: document.getElementById("sat-pred-filter"),
|
||||||
predUpcomingList: document.getElementById("sat-pred-list"),
|
predMinEl: document.getElementById("sat-pred-min-el"),
|
||||||
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
predCategory: document.getElementById("sat-pred-category"),
|
||||||
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
|
predCurrentList: document.getElementById("sat-pred-current-list"),
|
||||||
predStatus: document.getElementById("sat-pred-status")
|
predUpcomingList: document.getElementById("sat-pred-list"),
|
||||||
};
|
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
||||||
let satImageHistory = [];
|
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
|
||||||
const SAT_MAX_IMAGES = 100;
|
predStatus: document.getElementById("sat-pred-status")
|
||||||
const SAT_PRED_PAGE_SIZE = 50;
|
};
|
||||||
let satPredShowAll = false;
|
var satImageHistory = [];
|
||||||
let satFilterText = "";
|
var SAT_MAX_IMAGES = 100;
|
||||||
let satActiveView = "live";
|
var SAT_PRED_PAGE_SIZE = 50;
|
||||||
let satPredData = [];
|
var satPredShowAll = false;
|
||||||
let satPredFilterText = "";
|
var satFilterText = "";
|
||||||
let satPredMinEl = 0;
|
var satActiveView = "live";
|
||||||
let satPredCategory = "all";
|
var satPredData = [];
|
||||||
let satPredSatCount = 0;
|
var satPredFilterText = "";
|
||||||
let satPredCountdownTimer = null;
|
var satPredMinEl = 0;
|
||||||
function scheduleSatUi(key, job) {
|
var satPredCategory = "all";
|
||||||
if (typeof window.trxScheduleUiFrameJob === "function") {
|
var satPredSatCount = 0;
|
||||||
window.trxScheduleUiFrameJob(key, job);
|
var satPredCountdownTimer = null;
|
||||||
return;
|
function scheduleSatUi(key, job) {
|
||||||
}
|
if (typeof satWindow.trxScheduleUiFrameJob === "function") {
|
||||||
job();
|
satWindow.trxScheduleUiFrameJob(key, job);
|
||||||
}
|
|
||||||
function switchSatView(view) {
|
|
||||||
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
|
||||||
satActiveView = view;
|
|
||||||
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
|
|
||||||
if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
|
|
||||||
if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
|
|
||||||
if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
|
|
||||||
if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
|
|
||||||
if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
|
|
||||||
if (leavingPredictions) clearPredictionDom();
|
|
||||||
if (view === "history") {
|
|
||||||
renderSatHistoryTable();
|
|
||||||
} else if (view === "predictions") {
|
|
||||||
satPredShowAll = false;
|
|
||||||
loadSatPredictions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function clearPredictionDom() {
|
|
||||||
stopCountdownTimer();
|
|
||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
|
||||||
}
|
|
||||||
window.clearSatPredictionDom = clearPredictionDom;
|
|
||||||
satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
|
|
||||||
satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
|
|
||||||
satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
|
|
||||||
let _lastSatLrptOn = null;
|
|
||||||
window.updateSatLiveState = function(update) {
|
|
||||||
if (!satDom.lrptState) return;
|
|
||||||
const lrptOn = !!update.lrpt_decode_enabled;
|
|
||||||
if (lrptOn !== _lastSatLrptOn) {
|
|
||||||
_lastSatLrptOn = lrptOn;
|
|
||||||
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
|
|
||||||
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
|
||||||
if (satDom.status) {
|
|
||||||
if (lrptOn) {
|
|
||||||
satDom.status.textContent = "Decoder active — waiting for signal";
|
|
||||||
} else {
|
|
||||||
satDom.status.textContent = "Decoder idle";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function renderSatLatestCard() {
|
|
||||||
if (!satDom.liveLatest) return;
|
|
||||||
if (satImageHistory.length === 0) {
|
|
||||||
satDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const img = satImageHistory[0];
|
|
||||||
const decoder = img._decoder || "unknown";
|
|
||||||
const typeName = "Meteor LRPT";
|
|
||||||
const satellite = img.satellite || "";
|
|
||||||
const channels = img.channels || img.channel_a || "";
|
|
||||||
const lines = img.mcu_count || img.line_count || 0;
|
|
||||||
const unit = "MCU rows";
|
|
||||||
const ts = img._ts || "--";
|
|
||||||
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
|
||||||
let meta = [typeName];
|
|
||||||
if (satellite) meta.push(satellite);
|
|
||||||
if (channels) meta.push(channels);
|
|
||||||
meta.push(`${lines} ${unit}`);
|
|
||||||
meta.push(`${date} ${ts}`);
|
|
||||||
let html = `<div class="sat-latest-card">`;
|
|
||||||
html += `<div class="sat-latest-title">Latest decoded image</div>`;
|
|
||||||
html += `<div class="sat-latest-meta">${meta.join(" · ")}</div>`;
|
|
||||||
if (img.path) {
|
|
||||||
html += `<a href="${img.path}" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">Download PNG</a>`;
|
|
||||||
}
|
|
||||||
if (img.geo_bounds) {
|
|
||||||
html += ` <button type="button" class="sat-map-btn" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
satDom.liveLatest.innerHTML = html;
|
|
||||||
}
|
|
||||||
function getSatFilteredHistory() {
|
|
||||||
let items = satImageHistory;
|
|
||||||
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
|
||||||
if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
|
|
||||||
if (satFilterText) {
|
|
||||||
items = items.filter((i) => {
|
|
||||||
const haystack = [
|
|
||||||
"meteor lrpt",
|
|
||||||
i.satellite || "",
|
|
||||||
i.channels || "",
|
|
||||||
i.channel_a || "",
|
|
||||||
i.channel_b || ""
|
|
||||||
].join(" ").toUpperCase();
|
|
||||||
return haystack.includes(satFilterText);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
|
|
||||||
if (sortVal === "oldest") items = items.slice().reverse();
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
function renderSatHistoryRow(img) {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "sat-history-row";
|
|
||||||
const decoder = img._decoder || "unknown";
|
|
||||||
const typeName = "Meteor LRPT";
|
|
||||||
const typeClass = "sat-type-lrpt";
|
|
||||||
const ts = img._ts || "--";
|
|
||||||
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
|
|
||||||
const satellite = img.satellite || "--";
|
|
||||||
const channels = img.channels || "--";
|
|
||||||
const lines = img.mcu_count || img.line_count || 0;
|
|
||||||
const unit = "MCU";
|
|
||||||
let link = img.path ? `<a href="${img.path}" target="_blank" style="color:var(--accent);">PNG</a>` : "--";
|
|
||||||
if (img.geo_bounds) {
|
|
||||||
link += ` <a href="javascript:void(0)" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="color:var(--accent);">Map</a>`;
|
|
||||||
}
|
|
||||||
row.innerHTML = [
|
|
||||||
`<span>${date} ${ts}</span>`,
|
|
||||||
`<span class="sat-col-type ${typeClass}">${typeName}</span>`,
|
|
||||||
`<span>${satellite}</span>`,
|
|
||||||
`<span>${channels}</span>`,
|
|
||||||
`<span>${lines} ${unit}</span>`,
|
|
||||||
`<span>${link}</span>`
|
|
||||||
].join("");
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
function renderSatHistoryTable() {
|
|
||||||
if (!satDom.historyList) return;
|
|
||||||
const items = getSatFilteredHistory();
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
|
||||||
fragment.appendChild(renderSatHistoryRow(items[i]));
|
|
||||||
}
|
|
||||||
satDom.historyList.replaceChildren(fragment);
|
|
||||||
if (satDom.historyCount) {
|
|
||||||
const total = satImageHistory.length;
|
|
||||||
const shown = items.length;
|
|
||||||
satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function addSatImage(img, decoder) {
|
|
||||||
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
|
||||||
img._tsMs = tsMs;
|
|
||||||
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit"
|
|
||||||
});
|
|
||||||
img._decoder = decoder;
|
|
||||||
satImageHistory.unshift(img);
|
|
||||||
if (satImageHistory.length > SAT_MAX_IMAGES) {
|
|
||||||
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
|
|
||||||
}
|
|
||||||
scheduleSatUi("sat-latest", () => renderSatLatestCard());
|
|
||||||
if (satActiveView === "history") {
|
|
||||||
scheduleSatUi("sat-history", () => renderSatHistoryTable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.onServerLrptProgress = function(msg) {
|
|
||||||
if (satDom.status && msg.mcu_count > 0) {
|
|
||||||
satDom.status.textContent = "Receiving — " + msg.mcu_count + " MCU rows decoded";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.onServerLrptImage = function(msg) {
|
|
||||||
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
|
||||||
addSatImage(msg, "lrpt");
|
|
||||||
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
|
|
||||||
window.addSatMapOverlay(msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.resetSatHistoryView = function() {
|
|
||||||
satImageHistory = [];
|
|
||||||
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
|
||||||
renderSatLatestCard();
|
|
||||||
renderSatHistoryTable();
|
|
||||||
if (window.clearSatMapOverlays) window.clearSatMapOverlays();
|
|
||||||
};
|
|
||||||
window.pruneSatHistoryView = function() {
|
|
||||||
renderSatHistoryTable();
|
|
||||||
renderSatLatestCard();
|
|
||||||
};
|
|
||||||
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
|
||||||
lrptDecodeToggleBtn?.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
await window.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
|
||||||
await postPath("/toggle_lrpt_decode");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("LRPT toggle failed", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
satDom.filterInput?.addEventListener("input", () => {
|
|
||||||
satFilterText = satDom.filterInput.value.trim().toUpperCase();
|
|
||||||
renderSatHistoryTable();
|
|
||||||
});
|
|
||||||
satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
|
|
||||||
satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
|
|
||||||
document.getElementById("settings-clear-sat-history")?.addEventListener("click", async () => {
|
|
||||||
if (!await window.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
|
||||||
try {
|
|
||||||
await postPath("/clear_lrpt_decode");
|
|
||||||
window.resetSatHistoryView();
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Weather satellite history clear failed", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function azToCardinal(deg) {
|
|
||||||
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
|
||||||
return dirs[Math.round(deg / 45) % 8];
|
|
||||||
}
|
|
||||||
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 (satPredCountdownTimer) {
|
|
||||||
clearInterval(satPredCountdownTimer);
|
|
||||||
satPredCountdownTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function startCountdownTimer(container) {
|
|
||||||
const countdownEls = container ? container.querySelectorAll(".sat-pred-col-countdown") : [];
|
|
||||||
if (countdownEls.length === 0) return;
|
|
||||||
satPredCountdownTimer = setInterval(() => {
|
|
||||||
if (satActiveView !== "predictions") {
|
|
||||||
stopCountdownTimer();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = Date.now();
|
job();
|
||||||
let anyActive = false;
|
}
|
||||||
for (const el of countdownEls) {
|
function switchSatView(view) {
|
||||||
const los = parseInt(el.dataset.los, 10);
|
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
||||||
const rem = los - n;
|
satActiveView = view;
|
||||||
if (rem > 0) {
|
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
|
||||||
el.textContent = formatCountdown(rem);
|
if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
|
||||||
anyActive = true;
|
if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
|
||||||
} else {
|
if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
|
||||||
el.textContent = "0:00";
|
if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
|
||||||
|
if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
|
||||||
|
if (leavingPredictions) clearPredictionDom();
|
||||||
|
if (view === "history") {
|
||||||
|
renderSatHistoryTable();
|
||||||
|
} else if (view === "predictions") {
|
||||||
|
satPredShowAll = false;
|
||||||
|
void loadSatPredictions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function clearPredictionDom() {
|
||||||
|
stopCountdownTimer();
|
||||||
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
|
}
|
||||||
|
satWindow.clearSatPredictionDom = clearPredictionDom;
|
||||||
|
satDom.viewLiveBtn?.addEventListener("click", () => {
|
||||||
|
switchSatView("live");
|
||||||
|
});
|
||||||
|
satDom.viewHistoryBtn?.addEventListener("click", () => {
|
||||||
|
switchSatView("history");
|
||||||
|
});
|
||||||
|
satDom.viewPredBtn?.addEventListener("click", () => {
|
||||||
|
switchSatView("predictions");
|
||||||
|
});
|
||||||
|
var lastSatLrptOn = null;
|
||||||
|
satWindow.updateSatLiveState = function(update) {
|
||||||
|
if (!satDom.lrptState) return;
|
||||||
|
const lrptOn = !!update.lrpt_decode_enabled;
|
||||||
|
if (lrptOn !== lastSatLrptOn) {
|
||||||
|
lastSatLrptOn = lrptOn;
|
||||||
|
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
|
||||||
|
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
||||||
|
if (satDom.status) {
|
||||||
|
if (lrptOn) {
|
||||||
|
satDom.status.textContent = "Decoder active — waiting for signal";
|
||||||
|
} else {
|
||||||
|
satDom.status.textContent = "Decoder idle";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!anyActive) {
|
};
|
||||||
stopCountdownTimer();
|
function renderSatLatestCard() {
|
||||||
renderSatPredictions(getFilteredPredictions());
|
if (!satDom.liveLatest) return;
|
||||||
|
if (satImageHistory.length === 0) {
|
||||||
|
satDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, 1e3);
|
const img = satImageHistory[0];
|
||||||
}
|
if (!img) return;
|
||||||
function buildCurrentPassRow(pass, now) {
|
const typeName = "Meteor LRPT";
|
||||||
const row = document.createElement("div");
|
const satellite = img.satellite || "";
|
||||||
row.className = "sat-pred-row-current";
|
const channels = img.channels || img.channel_a || "";
|
||||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
|
const lines = img.mcu_count || img.line_count || 0;
|
||||||
const remaining = Math.max(0, pass.los_ms - now);
|
const unit = "MCU rows";
|
||||||
row.innerHTML = [
|
const ts = img._ts || "--";
|
||||||
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
||||||
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
|
const meta = [typeName];
|
||||||
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
|
if (satellite) meta.push(satellite);
|
||||||
`<span class="sat-pred-col-time">${formatPredTime(pass.los_ms)}</span>`,
|
if (channels) meta.push(channels);
|
||||||
`<span class="sat-pred-col-countdown" data-los="${pass.los_ms}">${formatCountdown(remaining)}</span>`,
|
meta.push(`${lines} ${unit}`);
|
||||||
`<span class="sat-pred-col-dir">${dir}</span>`
|
meta.push(`${date} ${ts}`);
|
||||||
].join("");
|
let html = `<div class="sat-latest-card">`;
|
||||||
return row;
|
html += `<div class="sat-latest-title">Latest decoded image</div>`;
|
||||||
}
|
html += `<div class="sat-latest-meta">${meta.join(" · ")}</div>`;
|
||||||
function buildUpcomingPassRow(pass) {
|
if (img.path) {
|
||||||
const row = document.createElement("div");
|
html += `<a href="${img.path}" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">Download PNG</a>`;
|
||||||
row.className = "sat-pred-row";
|
|
||||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
|
|
||||||
row.innerHTML = [
|
|
||||||
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
|
|
||||||
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
|
|
||||||
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
|
|
||||||
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
|
|
||||||
`<span class="sat-pred-col-dir">${dir}</span>`
|
|
||||||
].join("");
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
function getFilteredPredictions() {
|
|
||||||
let items = satPredData;
|
|
||||||
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
|
|
||||||
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
|
|
||||||
if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
function applyPredFilters() {
|
|
||||||
renderSatPredictions(getFilteredPredictions());
|
|
||||||
}
|
|
||||||
satDom.predFilter?.addEventListener("input", () => {
|
|
||||||
satPredFilterText = satDom.predFilter.value.trim().toUpperCase();
|
|
||||||
applyPredFilters();
|
|
||||||
});
|
|
||||||
satDom.predMinEl?.addEventListener("change", () => {
|
|
||||||
satPredMinEl = parseInt(satDom.predMinEl.value, 10) || 0;
|
|
||||||
applyPredFilters();
|
|
||||||
});
|
|
||||||
satDom.predCategory?.addEventListener("change", () => {
|
|
||||||
satPredCategory = satDom.predCategory.value;
|
|
||||||
applyPredFilters();
|
|
||||||
});
|
|
||||||
function renderSatPredictions(passes, error) {
|
|
||||||
stopCountdownTimer();
|
|
||||||
if (error) {
|
|
||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
|
||||||
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
|
|
||||||
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
|
|
||||||
if (satDom.predStatus) satDom.predStatus.textContent = error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(passes) || passes.length === 0) {
|
|
||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
|
||||||
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
|
|
||||||
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
|
|
||||||
if (satDom.predStatus) satDom.predStatus.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 (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
|
|
||||||
if (satDom.predCurrentList) {
|
|
||||||
if (current.length === 0) {
|
|
||||||
satDom.predCurrentList.innerHTML = "";
|
|
||||||
} else {
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
|
|
||||||
satDom.predCurrentList.replaceChildren(frag);
|
|
||||||
}
|
}
|
||||||
|
if (img.geo_bounds) {
|
||||||
|
html += ` <button type="button" class="sat-map-btn" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
satDom.liveLatest.innerHTML = html;
|
||||||
}
|
}
|
||||||
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
|
function getSatFilteredHistory() {
|
||||||
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
|
let items = satImageHistory;
|
||||||
const hiddenCount = upcoming.length - visibleUpcoming.length;
|
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
||||||
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
|
if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
|
||||||
if (satDom.predUpcomingList) {
|
if (satFilterText) {
|
||||||
const frag = document.createDocumentFragment();
|
items = items.filter((i) => {
|
||||||
for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
|
const haystack = [
|
||||||
if (hiddenCount > 0) {
|
"meteor lrpt",
|
||||||
const moreRow = document.createElement("div");
|
i.satellite || "",
|
||||||
moreRow.className = "sat-pred-row";
|
i.channels || "",
|
||||||
moreRow.style.cursor = "pointer";
|
i.channel_a || "",
|
||||||
moreRow.style.textAlign = "center";
|
i.channel_b || ""
|
||||||
moreRow.innerHTML = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes…</span>`;
|
].join(" ").toUpperCase();
|
||||||
moreRow.addEventListener("click", () => {
|
return haystack.includes(satFilterText);
|
||||||
satPredShowAll = true;
|
|
||||||
renderSatPredictions(getFilteredPredictions());
|
|
||||||
});
|
});
|
||||||
frag.appendChild(moreRow);
|
|
||||||
}
|
}
|
||||||
satDom.predUpcomingList.replaceChildren(frag);
|
const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
|
||||||
|
if (sortVal === "oldest") items = items.slice().reverse();
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
if (satDom.predStatus) {
|
function renderSatHistoryRow(img) {
|
||||||
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
|
const row = document.createElement("div");
|
||||||
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
|
row.className = "sat-history-row";
|
||||||
satDom.predStatus.textContent = text;
|
const typeName = "Meteor LRPT";
|
||||||
}
|
const typeClass = "sat-type-lrpt";
|
||||||
if (current.length > 0 && satActiveView === "predictions") {
|
const ts = img._ts || "--";
|
||||||
startCountdownTimer(satDom.predCurrentList);
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
|
||||||
}
|
const satellite = img.satellite || "--";
|
||||||
}
|
const channels = img.channels || "--";
|
||||||
async function loadSatPredictions() {
|
const lines = img.mcu_count || img.line_count || 0;
|
||||||
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
|
const unit = "MCU";
|
||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
let link = img.path ? `<a href="${img.path}" target="_blank" style="color:var(--accent);">PNG</a>` : "--";
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
if (img.geo_bounds) {
|
||||||
try {
|
link += ` <a href="javascript:void(0)" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="color:var(--accent);">Map</a>`;
|
||||||
const resp = await fetch("/sat_passes");
|
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
satPredSatCount = data.satellite_count || 0;
|
|
||||||
if (data.error) {
|
|
||||||
satPredData = [];
|
|
||||||
renderSatPredictions([], data.error);
|
|
||||||
} else {
|
|
||||||
satPredData = data.passes || [];
|
|
||||||
renderSatPredictions(getFilteredPredictions());
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
row.innerHTML = [
|
||||||
renderSatPredictions([], `Failed to load predictions: ${e.message}`);
|
`<span>${date} ${ts}</span>`,
|
||||||
|
`<span class="sat-col-type ${typeClass}">${typeName}</span>`,
|
||||||
|
`<span>${satellite}</span>`,
|
||||||
|
`<span>${channels}</span>`,
|
||||||
|
`<span>${lines} ${unit}</span>`,
|
||||||
|
`<span>${link}</span>`
|
||||||
|
].join("");
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
}
|
function renderSatHistoryTable() {
|
||||||
window.satShowOnMap = function(south, west, north, east) {
|
if (!satDom.historyList) return;
|
||||||
if (typeof window.enableMapSourceFilter === "function") {
|
const items = getSatFilteredHistory();
|
||||||
window.enableMapSourceFilter("sat");
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (const image of items) {
|
||||||
|
fragment.appendChild(renderSatHistoryRow(image));
|
||||||
|
}
|
||||||
|
satDom.historyList.replaceChildren(fragment);
|
||||||
|
if (satDom.historyCount) {
|
||||||
|
const total = satImageHistory.length;
|
||||||
|
const shown = items.length;
|
||||||
|
satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const lat = (south + north) / 2;
|
function addSatImage(img, decoder) {
|
||||||
const lon = (west + east) / 2;
|
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
||||||
if (window.navigateToAprsMap) {
|
img._tsMs = tsMs;
|
||||||
window.navigateToAprsMap(lat, lon);
|
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit"
|
||||||
|
});
|
||||||
|
img._decoder = decoder;
|
||||||
|
satImageHistory.unshift(img);
|
||||||
|
if (satImageHistory.length > SAT_MAX_IMAGES) {
|
||||||
|
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
|
||||||
|
}
|
||||||
|
scheduleSatUi("sat-latest", () => {
|
||||||
|
renderSatLatestCard();
|
||||||
|
});
|
||||||
|
if (satActiveView === "history") {
|
||||||
|
scheduleSatUi("sat-history", () => {
|
||||||
|
renderSatHistoryTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
satWindow.onServerLrptProgress = function(msg) {
|
||||||
renderSatLatestCard();
|
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
|
||||||
renderSatHistoryTable();
|
satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
satWindow.onServerLrptImage = function(msg) {
|
||||||
|
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
||||||
|
addSatImage(msg, "lrpt");
|
||||||
|
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
|
||||||
|
satWindow.addSatMapOverlay(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
satWindow.resetSatHistoryView = function() {
|
||||||
|
satImageHistory = [];
|
||||||
|
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
||||||
|
renderSatLatestCard();
|
||||||
|
renderSatHistoryTable();
|
||||||
|
satWindow.clearSatMapOverlays?.();
|
||||||
|
};
|
||||||
|
satWindow.pruneSatHistoryView = function() {
|
||||||
|
renderSatHistoryTable();
|
||||||
|
renderSatLatestCard();
|
||||||
|
};
|
||||||
|
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||||
|
lrptDecodeToggleBtn?.addEventListener("click", () => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
||||||
|
await satWindow.postPath?.("/toggle_lrpt_decode");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("LRPT toggle failed", e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
var satFilterInput = satDom.filterInput;
|
||||||
|
satFilterInput?.addEventListener("input", () => {
|
||||||
|
satFilterText = satFilterInput.value.trim().toUpperCase();
|
||||||
|
renderSatHistoryTable();
|
||||||
|
});
|
||||||
|
satDom.sortSelect?.addEventListener("change", () => {
|
||||||
|
renderSatHistoryTable();
|
||||||
|
});
|
||||||
|
satDom.typeFilter?.addEventListener("change", () => {
|
||||||
|
renderSatHistoryTable();
|
||||||
|
});
|
||||||
|
document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
|
||||||
|
void (async () => {
|
||||||
|
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
|
try {
|
||||||
|
await satWindow.postPath?.("/clear_lrpt_decode");
|
||||||
|
satWindow.resetSatHistoryView?.();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Weather satellite history clear failed", e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
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 (satPredCountdownTimer) {
|
||||||
|
clearInterval(satPredCountdownTimer);
|
||||||
|
satPredCountdownTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function startCountdownTimer(container) {
|
||||||
|
const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
|
||||||
|
if (countdownEls.length === 0) return;
|
||||||
|
satPredCountdownTimer = setInterval(() => {
|
||||||
|
if (satActiveView !== "predictions") {
|
||||||
|
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();
|
||||||
|
renderSatPredictions(getFilteredPredictions());
|
||||||
|
}
|
||||||
|
}, 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 = [
|
||||||
|
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
|
||||||
|
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
|
||||||
|
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
|
||||||
|
`<span class="sat-pred-col-time">${formatPredTime(pass.los_ms)}</span>`,
|
||||||
|
`<span class="sat-pred-col-countdown" data-los="${pass.los_ms}">${formatCountdown(remaining)}</span>`,
|
||||||
|
`<span class="sat-pred-col-dir">${dir}</span>`
|
||||||
|
].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 = [
|
||||||
|
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
|
||||||
|
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
|
||||||
|
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
|
||||||
|
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
|
||||||
|
`<span class="sat-pred-col-dir">${dir}</span>`
|
||||||
|
].join("");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
function getFilteredPredictions() {
|
||||||
|
let items = satPredData;
|
||||||
|
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
|
||||||
|
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
|
||||||
|
if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
function applyPredFilters() {
|
||||||
|
renderSatPredictions(getFilteredPredictions());
|
||||||
|
}
|
||||||
|
var satPredictionFilter = satDom.predFilter;
|
||||||
|
satPredictionFilter?.addEventListener("input", () => {
|
||||||
|
satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
|
||||||
|
applyPredFilters();
|
||||||
|
});
|
||||||
|
var satPredictionMinElevation = satDom.predMinEl;
|
||||||
|
satPredictionMinElevation?.addEventListener("change", () => {
|
||||||
|
satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
|
||||||
|
applyPredFilters();
|
||||||
|
});
|
||||||
|
var satPredictionCategory = satDom.predCategory;
|
||||||
|
satPredictionCategory?.addEventListener("change", () => {
|
||||||
|
satPredCategory = satPredictionCategory.value;
|
||||||
|
applyPredFilters();
|
||||||
|
});
|
||||||
|
function renderSatPredictions(passes, error) {
|
||||||
|
stopCountdownTimer();
|
||||||
|
if (error) {
|
||||||
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
|
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
|
||||||
|
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
|
||||||
|
if (satDom.predStatus) satDom.predStatus.textContent = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(passes) || passes.length === 0) {
|
||||||
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
|
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
|
||||||
|
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
|
||||||
|
if (satDom.predStatus) satDom.predStatus.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 (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
|
||||||
|
if (satDom.predCurrentList) {
|
||||||
|
if (current.length === 0) {
|
||||||
|
satDom.predCurrentList.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
|
||||||
|
satDom.predCurrentList.replaceChildren(frag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
|
||||||
|
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
|
||||||
|
const hiddenCount = upcoming.length - visibleUpcoming.length;
|
||||||
|
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
|
||||||
|
if (satDom.predUpcomingList) {
|
||||||
|
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 = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes…</span>`;
|
||||||
|
moreRow.addEventListener("click", () => {
|
||||||
|
satPredShowAll = true;
|
||||||
|
renderSatPredictions(getFilteredPredictions());
|
||||||
|
});
|
||||||
|
frag.appendChild(moreRow);
|
||||||
|
}
|
||||||
|
satDom.predUpcomingList.replaceChildren(frag);
|
||||||
|
}
|
||||||
|
if (satDom.predStatus) {
|
||||||
|
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
|
||||||
|
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
|
||||||
|
satDom.predStatus.textContent = text;
|
||||||
|
}
|
||||||
|
if (current.length > 0 && satActiveView === "predictions") {
|
||||||
|
startCountdownTimer(satDom.predCurrentList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadSatPredictions() {
|
||||||
|
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
|
||||||
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/sat_passes");
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
satPredSatCount = data.satellite_count || 0;
|
||||||
|
if (data.error) {
|
||||||
|
satPredData = [];
|
||||||
|
renderSatPredictions([], data.error);
|
||||||
|
} else {
|
||||||
|
satPredData = data.passes || [];
|
||||||
|
renderSatPredictions(getFilteredPredictions());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
satWindow.satShowOnMap = function(south, west, north, east) {
|
||||||
|
if (typeof satWindow.enableMapSourceFilter === "function") {
|
||||||
|
satWindow.enableMapSourceFilter("sat");
|
||||||
|
}
|
||||||
|
const lat = (south + north) / 2;
|
||||||
|
const lon = (west + east) / 2;
|
||||||
|
if (satWindow.navigateToAprsMap) {
|
||||||
|
satWindow.navigateToAprsMap(lat, lon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderSatLatestCard();
|
||||||
|
renderSatHistoryTable();
|
||||||
|
})();
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ await build({
|
|||||||
screenshot: path.join(sourceDir, "screenshot.ts"),
|
screenshot: path.join(sourceDir, "screenshot.ts"),
|
||||||
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
||||||
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
|
||||||
sat: path.join(sourceDir, "plugins", "sat.js"),
|
|
||||||
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
|
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
|
||||||
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
|
||||||
vchan: path.join(sourceDir, "plugins", "vchan.js"),
|
vchan: path.join(sourceDir, "plugins", "vchan.js"),
|
||||||
@@ -51,6 +50,7 @@ await build({
|
|||||||
ais: path.join(sourceDir, "plugins", "ais.ts"),
|
ais: path.join(sourceDir, "plugins", "ais.ts"),
|
||||||
aprs: path.join(sourceDir, "plugins", "aprs.ts"),
|
aprs: path.join(sourceDir, "plugins", "aprs.ts"),
|
||||||
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.ts"),
|
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.ts"),
|
||||||
|
sat: path.join(sourceDir, "plugins", "sat.ts"),
|
||||||
},
|
},
|
||||||
outdir: outputDir,
|
outdir: outputDir,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
|
|||||||
|
|
||||||
const loaded = new Set<string>();
|
const loaded = new Set<string>();
|
||||||
const loading = new Map<string, Promise<void>>();
|
const loading = new Map<string, Promise<void>>();
|
||||||
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js"]);
|
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js"]);
|
||||||
|
|
||||||
function loadLegacyScript(path: string): Promise<void> {
|
function loadLegacyScript(path: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
+109
-84
@@ -2,6 +2,28 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import type { LrptProgress, SatelliteImage, SatelliteLiveUpdate, SatellitePass, SatellitePassResponse } from "./satellite-types";
|
||||||
|
|
||||||
|
type SatelliteView = "live" | "history" | "predictions";
|
||||||
|
interface SatelliteBridge {
|
||||||
|
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
||||||
|
clearSatPredictionDom?: () => void;
|
||||||
|
updateSatLiveState?: (update: SatelliteLiveUpdate) => void;
|
||||||
|
onServerLrptProgress?: (message: LrptProgress) => void;
|
||||||
|
onServerLrptImage?: (message: SatelliteImage) => void;
|
||||||
|
addSatMapOverlay?: (image: SatelliteImage) => void;
|
||||||
|
resetSatHistoryView?: () => void;
|
||||||
|
pruneSatHistoryView?: () => void;
|
||||||
|
clearSatMapOverlays?: () => void;
|
||||||
|
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
||||||
|
postPath?: (path: string) => Promise<unknown>;
|
||||||
|
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
||||||
|
satShowOnMap?: (south: number, west: number, north: number, east: number) => void;
|
||||||
|
enableMapSourceFilter?: (source: string) => void;
|
||||||
|
navigateToAprsMap?: (lat: number, lon: number) => void;
|
||||||
|
}
|
||||||
|
const satWindow = window as unknown as SatelliteBridge;
|
||||||
|
|
||||||
// --- SAT Plugin ---
|
// --- SAT Plugin ---
|
||||||
// Live view: decoder state, latest image card
|
// Live view: decoder state, latest image card
|
||||||
// History view: filterable table of all decoded images
|
// History view: filterable table of all decoded images
|
||||||
@@ -16,16 +38,16 @@ const satDom = {
|
|||||||
liveLatest: document.getElementById("sat-live-latest"),
|
liveLatest: document.getElementById("sat-live-latest"),
|
||||||
historyList: document.getElementById("sat-history-list"),
|
historyList: document.getElementById("sat-history-list"),
|
||||||
historyCount: document.getElementById("sat-history-count"),
|
historyCount: document.getElementById("sat-history-count"),
|
||||||
filterInput: document.getElementById("sat-filter"),
|
filterInput: document.getElementById("sat-filter") as HTMLInputElement | null,
|
||||||
sortSelect: document.getElementById("sat-sort"),
|
sortSelect: document.getElementById("sat-sort") as HTMLSelectElement | null,
|
||||||
typeFilter: document.getElementById("sat-type-filter"),
|
typeFilter: document.getElementById("sat-type-filter") as HTMLSelectElement | null,
|
||||||
lrptState: document.getElementById("sat-lrpt-state"),
|
lrptState: document.getElementById("sat-lrpt-state"),
|
||||||
viewLiveBtn: document.getElementById("sat-view-live"),
|
viewLiveBtn: document.getElementById("sat-view-live"),
|
||||||
viewHistoryBtn: document.getElementById("sat-view-history"),
|
viewHistoryBtn: document.getElementById("sat-view-history"),
|
||||||
viewPredBtn: document.getElementById("sat-view-predictions"),
|
viewPredBtn: document.getElementById("sat-view-predictions"),
|
||||||
predFilter: document.getElementById("sat-pred-filter"),
|
predFilter: document.getElementById("sat-pred-filter") as HTMLInputElement | null,
|
||||||
predMinEl: document.getElementById("sat-pred-min-el"),
|
predMinEl: document.getElementById("sat-pred-min-el") as HTMLSelectElement | null,
|
||||||
predCategory: document.getElementById("sat-pred-category"),
|
predCategory: document.getElementById("sat-pred-category") as HTMLSelectElement | null,
|
||||||
predCurrentList: document.getElementById("sat-pred-current-list"),
|
predCurrentList: document.getElementById("sat-pred-current-list"),
|
||||||
predUpcomingList: document.getElementById("sat-pred-list"),
|
predUpcomingList: document.getElementById("sat-pred-list"),
|
||||||
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
||||||
@@ -34,30 +56,30 @@ const satDom = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── State ───────────────────────────────────────────────────────────
|
// ── State ───────────────────────────────────────────────────────────
|
||||||
let satImageHistory = [];
|
let satImageHistory: SatelliteImage[] = [];
|
||||||
const SAT_MAX_IMAGES = 100;
|
const SAT_MAX_IMAGES = 100;
|
||||||
const SAT_PRED_PAGE_SIZE = 50;
|
const SAT_PRED_PAGE_SIZE = 50;
|
||||||
let satPredShowAll = false;
|
let satPredShowAll = false;
|
||||||
let satFilterText = "";
|
let satFilterText = "";
|
||||||
let satActiveView = "live"; // "live" | "history" | "predictions"
|
let satActiveView: SatelliteView = "live";
|
||||||
let satPredData = [];
|
let satPredData: SatellitePass[] = [];
|
||||||
let satPredFilterText = "";
|
let satPredFilterText = "";
|
||||||
let satPredMinEl = 0;
|
let satPredMinEl = 0;
|
||||||
let satPredCategory = "all";
|
let satPredCategory = "all";
|
||||||
let satPredSatCount = 0;
|
let satPredSatCount = 0;
|
||||||
let satPredCountdownTimer = null;
|
let satPredCountdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
// ── UI scheduler helper ─────────────────────────────────────────────
|
// ── UI scheduler helper ─────────────────────────────────────────────
|
||||||
function scheduleSatUi(key, job) {
|
function scheduleSatUi(key: string, job: () => void): void {
|
||||||
if (typeof window.trxScheduleUiFrameJob === "function") {
|
if (typeof satWindow.trxScheduleUiFrameJob === "function") {
|
||||||
window.trxScheduleUiFrameJob(key, job);
|
satWindow.trxScheduleUiFrameJob(key, job);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
job();
|
job();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── View switching ──────────────────────────────────────────────────
|
// ── View switching ──────────────────────────────────────────────────
|
||||||
function switchSatView(view) {
|
function switchSatView(view: SatelliteView): void {
|
||||||
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
||||||
satActiveView = view;
|
satActiveView = view;
|
||||||
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
|
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
|
||||||
@@ -71,7 +93,7 @@ function switchSatView(view) {
|
|||||||
renderSatHistoryTable();
|
renderSatHistoryTable();
|
||||||
} else if (view === "predictions") {
|
} else if (view === "predictions") {
|
||||||
satPredShowAll = false;
|
satPredShowAll = false;
|
||||||
loadSatPredictions();
|
void loadSatPredictions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,19 +102,19 @@ function clearPredictionDom() {
|
|||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
}
|
}
|
||||||
window.clearSatPredictionDom = clearPredictionDom;
|
satWindow.clearSatPredictionDom = clearPredictionDom;
|
||||||
|
|
||||||
satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
|
satDom.viewLiveBtn?.addEventListener("click", () => { switchSatView("live"); });
|
||||||
satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
|
satDom.viewHistoryBtn?.addEventListener("click", () => { switchSatView("history"); });
|
||||||
satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
|
satDom.viewPredBtn?.addEventListener("click", () => { switchSatView("predictions"); });
|
||||||
|
|
||||||
// ── Live view: decoder state ────────────────────────────────────────
|
// ── Live view: decoder state ────────────────────────────────────────
|
||||||
let _lastSatLrptOn = null;
|
let lastSatLrptOn: boolean | null = null;
|
||||||
window.updateSatLiveState = function (update) {
|
satWindow.updateSatLiveState = function (update: SatelliteLiveUpdate) {
|
||||||
if (!satDom.lrptState) return;
|
if (!satDom.lrptState) return;
|
||||||
const lrptOn = !!update.lrpt_decode_enabled;
|
const lrptOn = !!update.lrpt_decode_enabled;
|
||||||
if (lrptOn !== _lastSatLrptOn) {
|
if (lrptOn !== lastSatLrptOn) {
|
||||||
_lastSatLrptOn = lrptOn;
|
lastSatLrptOn = lrptOn;
|
||||||
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
|
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
|
||||||
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
||||||
if (satDom.status) {
|
if (satDom.status) {
|
||||||
@@ -114,7 +136,7 @@ function renderSatLatestCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const img = satImageHistory[0];
|
const img = satImageHistory[0];
|
||||||
const decoder = img._decoder || "unknown";
|
if (!img) return;
|
||||||
const typeName = "Meteor LRPT";
|
const typeName = "Meteor LRPT";
|
||||||
const satellite = img.satellite || "";
|
const satellite = img.satellite || "";
|
||||||
const channels = img.channels || img.channel_a || "";
|
const channels = img.channels || img.channel_a || "";
|
||||||
@@ -123,7 +145,7 @@ function renderSatLatestCard() {
|
|||||||
const ts = img._ts || "--";
|
const ts = img._ts || "--";
|
||||||
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
|
||||||
|
|
||||||
let meta = [typeName];
|
const meta = [typeName];
|
||||||
if (satellite) meta.push(satellite);
|
if (satellite) meta.push(satellite);
|
||||||
if (channels) meta.push(channels);
|
if (channels) meta.push(channels);
|
||||||
meta.push(`${lines} ${unit}`);
|
meta.push(`${lines} ${unit}`);
|
||||||
@@ -143,7 +165,7 @@ function renderSatLatestCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── History view: table ─────────────────────────────────────────────
|
// ── History view: table ─────────────────────────────────────────────
|
||||||
function getSatFilteredHistory() {
|
function getSatFilteredHistory(): SatelliteImage[] {
|
||||||
let items = satImageHistory;
|
let items = satImageHistory;
|
||||||
|
|
||||||
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
||||||
@@ -168,11 +190,10 @@ function getSatFilteredHistory() {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSatHistoryRow(img) {
|
function renderSatHistoryRow(img: SatelliteImage): HTMLElement {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "sat-history-row";
|
row.className = "sat-history-row";
|
||||||
|
|
||||||
const decoder = img._decoder || "unknown";
|
|
||||||
const typeName = "Meteor LRPT";
|
const typeName = "Meteor LRPT";
|
||||||
const typeClass = "sat-type-lrpt";
|
const typeClass = "sat-type-lrpt";
|
||||||
const ts = img._ts || "--";
|
const ts = img._ts || "--";
|
||||||
@@ -204,8 +225,8 @@ function renderSatHistoryTable() {
|
|||||||
if (!satDom.historyList) return;
|
if (!satDom.historyList) return;
|
||||||
const items = getSatFilteredHistory();
|
const items = getSatFilteredHistory();
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
for (const image of items) {
|
||||||
fragment.appendChild(renderSatHistoryRow(items[i]));
|
fragment.appendChild(renderSatHistoryRow(image));
|
||||||
}
|
}
|
||||||
satDom.historyList.replaceChildren(fragment);
|
satDom.historyList.replaceChildren(fragment);
|
||||||
|
|
||||||
@@ -222,7 +243,7 @@ function renderSatHistoryTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Add image to history ────────────────────────────────────────────
|
// ── Add image to history ────────────────────────────────────────────
|
||||||
function addSatImage(img, decoder) {
|
function addSatImage(img: SatelliteImage, decoder: "lrpt"): void {
|
||||||
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
|
||||||
img._tsMs = tsMs;
|
img._tsMs = tsMs;
|
||||||
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
img._ts = new Date(tsMs).toLocaleTimeString([], {
|
||||||
@@ -237,102 +258,103 @@ function addSatImage(img, decoder) {
|
|||||||
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
|
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleSatUi("sat-latest", () => renderSatLatestCard());
|
scheduleSatUi("sat-latest", () => { renderSatLatestCard(); });
|
||||||
if (satActiveView === "history") {
|
if (satActiveView === "history") {
|
||||||
scheduleSatUi("sat-history", () => renderSatHistoryTable());
|
scheduleSatUi("sat-history", () => { renderSatHistoryTable(); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Server callbacks ────────────────────────────────────────────────
|
// ── Server callbacks ────────────────────────────────────────────────
|
||||||
window.onServerLrptProgress = function (msg) {
|
satWindow.onServerLrptProgress = function (msg: LrptProgress) {
|
||||||
if (satDom.status && msg.mcu_count > 0) {
|
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
|
||||||
satDom.status.textContent = "Receiving \u2014 " + msg.mcu_count + " MCU rows decoded";
|
satDom.status.textContent = `Receiving \u2014 ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onServerLrptImage = function (msg) {
|
satWindow.onServerLrptImage = function (msg: SatelliteImage) {
|
||||||
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
||||||
addSatImage(msg, "lrpt");
|
addSatImage(msg, "lrpt");
|
||||||
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
|
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
|
||||||
window.addSatMapOverlay(msg);
|
satWindow.addSatMapOverlay(msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.resetSatHistoryView = function () {
|
satWindow.resetSatHistoryView = function () {
|
||||||
satImageHistory = [];
|
satImageHistory = [];
|
||||||
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
||||||
renderSatLatestCard();
|
renderSatLatestCard();
|
||||||
renderSatHistoryTable();
|
renderSatHistoryTable();
|
||||||
if (window.clearSatMapOverlays) window.clearSatMapOverlays();
|
satWindow.clearSatMapOverlays?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.pruneSatHistoryView = function () {
|
satWindow.pruneSatHistoryView = function () {
|
||||||
renderSatHistoryTable();
|
renderSatHistoryTable();
|
||||||
renderSatLatestCard();
|
renderSatLatestCard();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Toggle buttons ──────────────────────────────────────────────────
|
// ── Toggle buttons ──────────────────────────────────────────────────
|
||||||
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
|
||||||
lrptDecodeToggleBtn?.addEventListener("click", async () => {
|
lrptDecodeToggleBtn?.addEventListener("click", () => { void (async () => {
|
||||||
try {
|
try {
|
||||||
await window.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
|
||||||
await postPath("/toggle_lrpt_decode");
|
await satWindow.postPath?.("/toggle_lrpt_decode");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("LRPT toggle failed", e);
|
console.error("LRPT toggle failed", e);
|
||||||
}
|
}
|
||||||
});
|
})(); });
|
||||||
|
|
||||||
// ── Filter / sort event listeners ───────────────────────────────────
|
// ── Filter / sort event listeners ───────────────────────────────────
|
||||||
satDom.filterInput?.addEventListener("input", () => {
|
const satFilterInput = satDom.filterInput;
|
||||||
satFilterText = satDom.filterInput.value.trim().toUpperCase();
|
satFilterInput?.addEventListener("input", () => {
|
||||||
|
satFilterText = satFilterInput.value.trim().toUpperCase();
|
||||||
renderSatHistoryTable();
|
renderSatHistoryTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
|
satDom.sortSelect?.addEventListener("change", () => { renderSatHistoryTable(); });
|
||||||
satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
|
satDom.typeFilter?.addEventListener("change", () => { renderSatHistoryTable(); });
|
||||||
|
|
||||||
// ── Settings: clear history ─────────────────────────────────────────
|
// ── Settings: clear history ─────────────────────────────────────────
|
||||||
document
|
document
|
||||||
.getElementById("settings-clear-sat-history")
|
.getElementById("settings-clear-sat-history")
|
||||||
?.addEventListener("click", async () => {
|
?.addEventListener("click", () => { void (async () => {
|
||||||
if (!await window.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
|
||||||
try {
|
try {
|
||||||
await postPath("/clear_lrpt_decode");
|
await satWindow.postPath?.("/clear_lrpt_decode");
|
||||||
window.resetSatHistoryView();
|
satWindow.resetSatHistoryView?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Weather satellite history clear failed", e);
|
console.error("Weather satellite history clear failed", e);
|
||||||
}
|
}
|
||||||
});
|
})(); });
|
||||||
|
|
||||||
// ── Predictions: helpers ────────────────────────────────────────────
|
// ── Predictions: helpers ────────────────────────────────────────────
|
||||||
function azToCardinal(deg) {
|
function azToCardinal(deg: number): string {
|
||||||
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
||||||
return dirs[Math.round(deg / 45) % 8];
|
return dirs[Math.round(deg / 45) % 8] ?? "N";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPredTime(ms) {
|
function formatPredTime(ms: number): string {
|
||||||
const d = new Date(ms);
|
const d = new Date(ms);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
const day = d.getUTCDay() !== now.getUTCDay() ? dayNames[d.getUTCDay()] + " " : "";
|
const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
|
||||||
const hh = String(d.getUTCHours()).padStart(2, "0");
|
const hh = String(d.getUTCHours()).padStart(2, "0");
|
||||||
const mm = String(d.getUTCMinutes()).padStart(2, "0");
|
const mm = String(d.getUTCMinutes()).padStart(2, "0");
|
||||||
return `${day}${hh}:${mm}`;
|
return `${day}${hh}:${mm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPredDuration(s) {
|
function formatPredDuration(s: number): string {
|
||||||
if (s >= 60) return `${Math.round(s / 60)} min`;
|
if (s >= 60) return `${Math.round(s / 60)} min`;
|
||||||
return `${s}s`;
|
return `${s}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCountdown(ms) {
|
function formatCountdown(ms: number): string {
|
||||||
const totalSec = Math.max(0, Math.floor(ms / 1000));
|
const totalSec = Math.max(0, Math.floor(ms / 1000));
|
||||||
const m = Math.floor(totalSec / 60);
|
const m = Math.floor(totalSec / 60);
|
||||||
const s = totalSec % 60;
|
const s = totalSec % 60;
|
||||||
return `${m}:${String(s).padStart(2, "0")}`;
|
return `${m}:${String(s).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function elevationClass(deg) {
|
function elevationClass(deg: number): string {
|
||||||
if (deg >= 45) return "sat-pred-el-high";
|
if (deg >= 45) return "sat-pred-el-high";
|
||||||
if (deg >= 10) return "sat-pred-el-mid";
|
if (deg >= 10) return "sat-pred-el-mid";
|
||||||
return "sat-pred-el-low";
|
return "sat-pred-el-low";
|
||||||
@@ -346,8 +368,8 @@ function stopCountdownTimer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCountdownTimer(container) {
|
function startCountdownTimer(container: HTMLElement | null): void {
|
||||||
const countdownEls = container ? container.querySelectorAll(".sat-pred-col-countdown") : [];
|
const countdownEls = container?.querySelectorAll<HTMLElement>(".sat-pred-col-countdown") ?? [];
|
||||||
if (countdownEls.length === 0) return;
|
if (countdownEls.length === 0) return;
|
||||||
|
|
||||||
satPredCountdownTimer = setInterval(() => {
|
satPredCountdownTimer = setInterval(() => {
|
||||||
@@ -358,7 +380,7 @@ function startCountdownTimer(container) {
|
|||||||
const n = Date.now();
|
const n = Date.now();
|
||||||
let anyActive = false;
|
let anyActive = false;
|
||||||
for (const el of countdownEls) {
|
for (const el of countdownEls) {
|
||||||
const los = parseInt(el.dataset.los, 10);
|
const los = Number.parseInt(el.dataset.los ?? "0", 10);
|
||||||
const rem = los - n;
|
const rem = los - n;
|
||||||
if (rem > 0) {
|
if (rem > 0) {
|
||||||
el.textContent = formatCountdown(rem);
|
el.textContent = formatCountdown(rem);
|
||||||
@@ -375,7 +397,7 @@ function startCountdownTimer(container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Predictions: row builders ───────────────────────────────────────
|
// ── Predictions: row builders ───────────────────────────────────────
|
||||||
function buildCurrentPassRow(pass, now) {
|
function buildCurrentPassRow(pass: SatellitePass, now: number): HTMLElement {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "sat-pred-row-current";
|
row.className = "sat-pred-row-current";
|
||||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
|
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
|
||||||
@@ -391,7 +413,7 @@ function buildCurrentPassRow(pass, now) {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUpcomingPassRow(pass) {
|
function buildUpcomingPassRow(pass: SatellitePass): HTMLElement {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "sat-pred-row";
|
row.className = "sat-pred-row";
|
||||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
|
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
|
||||||
@@ -406,7 +428,7 @@ function buildUpcomingPassRow(pass) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Predictions: filter state ───────────────────────────────────────
|
// ── Predictions: filter state ───────────────────────────────────────
|
||||||
function getFilteredPredictions() {
|
function getFilteredPredictions(): SatellitePass[] {
|
||||||
let items = satPredData;
|
let items = satPredData;
|
||||||
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
|
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
|
||||||
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
|
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
|
||||||
@@ -418,23 +440,26 @@ function applyPredFilters() {
|
|||||||
renderSatPredictions(getFilteredPredictions());
|
renderSatPredictions(getFilteredPredictions());
|
||||||
}
|
}
|
||||||
|
|
||||||
satDom.predFilter?.addEventListener("input", () => {
|
const satPredictionFilter = satDom.predFilter;
|
||||||
satPredFilterText = satDom.predFilter.value.trim().toUpperCase();
|
satPredictionFilter?.addEventListener("input", () => {
|
||||||
|
satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
|
||||||
applyPredFilters();
|
applyPredFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
satDom.predMinEl?.addEventListener("change", () => {
|
const satPredictionMinElevation = satDom.predMinEl;
|
||||||
satPredMinEl = parseInt(satDom.predMinEl.value, 10) || 0;
|
satPredictionMinElevation?.addEventListener("change", () => {
|
||||||
|
satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
|
||||||
applyPredFilters();
|
applyPredFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
satDom.predCategory?.addEventListener("change", () => {
|
const satPredictionCategory = satDom.predCategory;
|
||||||
satPredCategory = satDom.predCategory.value;
|
satPredictionCategory?.addEventListener("change", () => {
|
||||||
|
satPredCategory = satPredictionCategory.value;
|
||||||
applyPredFilters();
|
applyPredFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Predictions: main render ────────────────────────────────────────
|
// ── Predictions: main render ────────────────────────────────────────
|
||||||
function renderSatPredictions(passes, error) {
|
function renderSatPredictions(passes: SatellitePass[], error?: string): void {
|
||||||
stopCountdownTimer();
|
stopCountdownTimer();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -508,14 +533,14 @@ function renderSatPredictions(passes, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Predictions: data loading ───────────────────────────────────────
|
// ── Predictions: data loading ───────────────────────────────────────
|
||||||
async function loadSatPredictions() {
|
async function loadSatPredictions(): Promise<void> {
|
||||||
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions\u2026";
|
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions\u2026";
|
||||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/sat_passes");
|
const resp = await fetch("/sat_passes");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json() as SatellitePassResponse;
|
||||||
satPredSatCount = data.satellite_count || 0;
|
satPredSatCount = data.satellite_count || 0;
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
satPredData = [];
|
satPredData = [];
|
||||||
@@ -524,20 +549,20 @@ async function loadSatPredictions() {
|
|||||||
satPredData = data.passes || [];
|
satPredData = data.passes || [];
|
||||||
renderSatPredictions(getFilteredPredictions());
|
renderSatPredictions(getFilteredPredictions());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error: unknown) {
|
||||||
renderSatPredictions([], `Failed to load predictions: ${e.message}`);
|
renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Navigate to map centered on satellite image bounds ──────────────
|
// ── Navigate to map centered on satellite image bounds ──────────────
|
||||||
window.satShowOnMap = function (south, west, north, east) {
|
satWindow.satShowOnMap = function (south: number, west: number, north: number, east: number) {
|
||||||
if (typeof window.enableMapSourceFilter === "function") {
|
if (typeof satWindow.enableMapSourceFilter === "function") {
|
||||||
window.enableMapSourceFilter("sat");
|
satWindow.enableMapSourceFilter("sat");
|
||||||
}
|
}
|
||||||
const lat = (south + north) / 2;
|
const lat = (south + north) / 2;
|
||||||
const lon = (west + east) / 2;
|
const lon = (west + east) / 2;
|
||||||
if (window.navigateToAprsMap) {
|
if (satWindow.navigateToAprsMap) {
|
||||||
window.navigateToAprsMap(lat, lon);
|
satWindow.navigateToAprsMap(lat, lon);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
export interface SatelliteImage {
|
||||||
|
ts_ms?: number | null;
|
||||||
|
satellite?: string;
|
||||||
|
channels?: string;
|
||||||
|
channel_a?: string;
|
||||||
|
channel_b?: string;
|
||||||
|
mcu_count?: number;
|
||||||
|
line_count?: number;
|
||||||
|
path?: string;
|
||||||
|
geo_bounds?: [number, number, number, number];
|
||||||
|
_tsMs?: number;
|
||||||
|
_ts?: string;
|
||||||
|
_decoder?: "lrpt";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LrptProgress { mcu_count?: number }
|
||||||
|
export interface SatelliteLiveUpdate { lrpt_decode_enabled?: boolean }
|
||||||
|
|
||||||
|
export interface SatellitePass {
|
||||||
|
satellite: string;
|
||||||
|
category: string;
|
||||||
|
aos_ms: number;
|
||||||
|
los_ms: number;
|
||||||
|
duration_s: number;
|
||||||
|
azimuth_aos_deg: number;
|
||||||
|
azimuth_los_deg: number;
|
||||||
|
max_elevation_deg: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SatellitePassResponse {
|
||||||
|
satellite_count?: number;
|
||||||
|
error?: string | null;
|
||||||
|
passes?: SatellitePass[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SatelliteScheduleEntry {
|
||||||
|
id: string;
|
||||||
|
satellite: string;
|
||||||
|
norad_id: number;
|
||||||
|
bookmark_id: string;
|
||||||
|
min_elevation_deg: number;
|
||||||
|
priority: number;
|
||||||
|
center_hz: number | null;
|
||||||
|
bookmark_ids: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SatelliteScheduleConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
pretune_secs: number;
|
||||||
|
entries: SatelliteScheduleEntry[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import test from "node:test";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
test("satellite entry registers lifecycle callbacks and forwards georeferenced images", async () => {
|
||||||
|
const overlays = [];
|
||||||
|
const window = {
|
||||||
|
trxUi: { confirm: async () => true },
|
||||||
|
addSatMapOverlay: (image) => { overlays.push(image); },
|
||||||
|
};
|
||||||
|
const context = vm.createContext({
|
||||||
|
window,
|
||||||
|
document: { getElementById: () => null, createDocumentFragment: () => ({ appendChild() {} }) },
|
||||||
|
fetch: async () => ({ ok: true, json: async () => ({ satellite_count: 0, passes: [] }) }),
|
||||||
|
setInterval: () => 1,
|
||||||
|
clearInterval() {},
|
||||||
|
Date,
|
||||||
|
Number,
|
||||||
|
String,
|
||||||
|
Array,
|
||||||
|
Error,
|
||||||
|
console,
|
||||||
|
});
|
||||||
|
const source = await readFile(new URL("../../assets/web/generated/sat.js", import.meta.url), "utf8");
|
||||||
|
new vm.Script(source).runInContext(context);
|
||||||
|
|
||||||
|
window.onServerLrptImage({ path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 });
|
||||||
|
assert.equal(overlays.length, 1);
|
||||||
|
assert.deepEqual(overlays[0].geo_bounds, [50, 10, 55, 20]);
|
||||||
|
assert.equal(typeof window.updateSatLiveState, "function");
|
||||||
|
assert.equal(typeof window.resetSatHistoryView, "function");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user