[refactor](trx-frontend-http): extract satellite scheduling UI into dedicated module
Move ~230 lines of satellite pass scheduling code from scheduler.js into a new sat-scheduler.js plugin with cached DOM refs, createElement- based rendering, and a clean bridge API. Refactor sat.js predictions view to deduplicate row builders, extract countdown timer lifecycle management, and cache all DOM references. https://claude.ai/code/session_0144nUfHAKs7yRnYTsozNagw Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1393,6 +1393,7 @@
|
||||
<script src="/sat.js"></script>
|
||||
<script src="/bookmarks.js"></script>
|
||||
<script src="/scheduler.js"></script>
|
||||
<script src="/sat-scheduler.js"></script>
|
||||
<script src="/background-decode.js"></script>
|
||||
<script src="/vchan.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// Satellite Pass Scheduling UI
|
||||
// Manages the satellite overlay section within the background decoding scheduler.
|
||||
// Communicates with scheduler.js via a thin window API for shared state access.
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── DOM references (cached once) ──────────────────────────────────
|
||||
const dom = {
|
||||
enabled: document.getElementById("scheduler-sat-enabled"),
|
||||
pretune: document.getElementById("scheduler-sat-pretune"),
|
||||
body: document.getElementById("scheduler-sat-body"),
|
||||
tbody: document.getElementById("scheduler-sat-tbody"),
|
||||
addBtn: document.getElementById("scheduler-sat-add-btn"),
|
||||
passStatus: document.getElementById("scheduler-sat-pass-status"),
|
||||
formWrap: document.getElementById("sch-sat-form-wrap"),
|
||||
formTitle: document.getElementById("sch-sat-form-title"),
|
||||
form: document.getElementById("sch-sat-form"),
|
||||
formCancel: document.getElementById("sch-sat-form-cancel"),
|
||||
preset: document.getElementById("scheduler-sat-preset"),
|
||||
name: document.getElementById("scheduler-sat-name"),
|
||||
norad: document.getElementById("scheduler-sat-norad"),
|
||||
bookmark: document.getElementById("scheduler-sat-bookmark"),
|
||||
minEl: document.getElementById("scheduler-sat-min-el"),
|
||||
priority: document.getElementById("scheduler-sat-priority"),
|
||||
centerHz: document.getElementById("scheduler-sat-center-hz"),
|
||||
};
|
||||
|
||||
// ── Local state ───────────────────────────────────────────────────
|
||||
let editIdx = null; // null = adding, number = editing
|
||||
|
||||
// ── Scheduler bridge ──────────────────────────────────────────────
|
||||
// These accessors call into scheduler.js via window.schedulerBridge,
|
||||
// which is set up by scheduler.js after it initializes.
|
||||
function getBridge() {
|
||||
return window.schedulerBridge || {};
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
const b = getBridge();
|
||||
return typeof b.getConfig === "function" ? b.getConfig() : null;
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
const b = getBridge();
|
||||
return typeof b.getStatus === "function" ? b.getStatus() : null;
|
||||
}
|
||||
|
||||
function getBookmarks() {
|
||||
const b = getBridge();
|
||||
return typeof b.getBookmarks === "function" ? b.getBookmarks() : [];
|
||||
}
|
||||
|
||||
function bmName(id) {
|
||||
const bm = getBookmarks().find(function (b) { return b.id === id; });
|
||||
return bm ? bm.name : String(id || "");
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function formatFreq(hz) {
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
|
||||
return hz + " Hz";
|
||||
}
|
||||
|
||||
// ── Satellite config helpers ──────────────────────────────────────
|
||||
function getSatelliteEntries() {
|
||||
var config = getConfig();
|
||||
return (config && config.satellites && Array.isArray(config.satellites.entries))
|
||||
? config.satellites.entries
|
||||
: [];
|
||||
}
|
||||
|
||||
function ensureSatelliteConfig() {
|
||||
var config = getConfig();
|
||||
if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
|
||||
if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
|
||||
if (!config.satellites.entries) config.satellites.entries = [];
|
||||
return config.satellites;
|
||||
}
|
||||
|
||||
function collectSatelliteConfig() {
|
||||
var enabled = dom.enabled ? dom.enabled.checked : false;
|
||||
var pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
|
||||
return {
|
||||
enabled: enabled,
|
||||
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
|
||||
entries: getSatelliteEntries(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Render: section ───────────────────────────────────────────────
|
||||
function renderSection() {
|
||||
var config = getConfig();
|
||||
var satCfg = (config && config.satellites) || {};
|
||||
var enabled = !!satCfg.enabled;
|
||||
|
||||
if (dom.enabled) dom.enabled.checked = enabled;
|
||||
if (dom.pretune) dom.pretune.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60;
|
||||
if (dom.body) dom.body.style.display = enabled ? "" : "none";
|
||||
|
||||
renderEntries();
|
||||
renderPassStatus();
|
||||
}
|
||||
|
||||
// ── Render: entries table ─────────────────────────────────────────
|
||||
function renderEntries() {
|
||||
if (!dom.tbody) return;
|
||||
var entries = getSatelliteEntries();
|
||||
var frag = document.createDocumentFragment();
|
||||
|
||||
entries.forEach(function (entry, idx) {
|
||||
var tr = document.createElement("tr");
|
||||
|
||||
var tdSat = document.createElement("td");
|
||||
tdSat.textContent = entry.satellite || "";
|
||||
tr.appendChild(tdSat);
|
||||
|
||||
var tdNorad = document.createElement("td");
|
||||
tdNorad.textContent = entry.norad_id || "";
|
||||
tr.appendChild(tdNorad);
|
||||
|
||||
var tdBm = document.createElement("td");
|
||||
tdBm.textContent = bmName(entry.bookmark_id);
|
||||
tr.appendChild(tdBm);
|
||||
|
||||
var tdEl = document.createElement("td");
|
||||
tdEl.textContent = (entry.min_elevation_deg != null ? entry.min_elevation_deg + "\u00B0" : "5\u00B0");
|
||||
tr.appendChild(tdEl);
|
||||
|
||||
var tdPrio = document.createElement("td");
|
||||
tdPrio.textContent = entry.priority || 0;
|
||||
tr.appendChild(tdPrio);
|
||||
|
||||
var tdActions = document.createElement("td");
|
||||
|
||||
var editBtn = document.createElement("button");
|
||||
editBtn.className = "sch-write";
|
||||
editBtn.type = "button";
|
||||
editBtn.textContent = "Edit";
|
||||
editBtn.addEventListener("click", function () {
|
||||
openForm(entry, idx);
|
||||
});
|
||||
tdActions.appendChild(editBtn);
|
||||
|
||||
var removeBtn = document.createElement("button");
|
||||
removeBtn.className = "sch-write";
|
||||
removeBtn.type = "button";
|
||||
removeBtn.textContent = "Remove";
|
||||
removeBtn.addEventListener("click", function () {
|
||||
removeEntry(idx);
|
||||
});
|
||||
tdActions.appendChild(removeBtn);
|
||||
|
||||
tr.appendChild(tdActions);
|
||||
frag.appendChild(tr);
|
||||
});
|
||||
|
||||
dom.tbody.replaceChildren(frag);
|
||||
}
|
||||
|
||||
// ── Render: pass status ───────────────────────────────────────────
|
||||
function renderPassStatus() {
|
||||
if (!dom.passStatus) return;
|
||||
var entries = getSatelliteEntries();
|
||||
if (entries.length === 0) {
|
||||
dom.passStatus.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
var status = getStatus();
|
||||
if (status && status.active_satellite) {
|
||||
dom.passStatus.innerHTML =
|
||||
'<span class="sch-sat-active-badge">PASS ACTIVE: ' +
|
||||
escHtml(status.active_satellite) +
|
||||
'</span>';
|
||||
} else {
|
||||
dom.passStatus.innerHTML =
|
||||
'<span style="color:var(--text-muted);font-size:0.8rem;">No satellite pass active. Predictions available in the SAT tab.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render: bookmark dropdown ─────────────────────────────────────
|
||||
function renderBookmarkSelect(selectedId) {
|
||||
if (!dom.bookmark) return;
|
||||
dom.bookmark.innerHTML = '<option value="">— none —</option>';
|
||||
getBookmarks().forEach(function (bm) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = bm.id;
|
||||
opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
|
||||
if (bm.id === selectedId) opt.selected = true;
|
||||
dom.bookmark.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Entry management ──────────────────────────────────────────────
|
||||
function removeEntry(idx) {
|
||||
var sat = ensureSatelliteConfig();
|
||||
sat.entries.splice(idx, 1);
|
||||
renderEntries();
|
||||
}
|
||||
|
||||
// ── Form: open ────────────────────────────────────────────────────
|
||||
function openForm(entry, idx) {
|
||||
editIdx = (idx != null) ? idx : null;
|
||||
|
||||
if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
|
||||
if (dom.preset) dom.preset.value = "";
|
||||
if (dom.name) dom.name.value = entry ? (entry.satellite || "") : "";
|
||||
if (dom.norad) dom.norad.value = entry ? (entry.norad_id || "") : "";
|
||||
if (dom.minEl) dom.minEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5;
|
||||
if (dom.priority) dom.priority.value = entry && entry.priority != null ? entry.priority : 0;
|
||||
if (dom.centerHz) dom.centerHz.value = entry && entry.center_hz ? entry.center_hz : "";
|
||||
|
||||
renderBookmarkSelect(entry ? entry.bookmark_id : null);
|
||||
|
||||
if (dom.formWrap) {
|
||||
dom.formWrap.style.display = "flex";
|
||||
if (dom.name) dom.name.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form: close ───────────────────────────────────────────────────
|
||||
function closeForm() {
|
||||
if (dom.formWrap) dom.formWrap.style.display = "none";
|
||||
editIdx = null;
|
||||
}
|
||||
|
||||
// ── Form: submit ──────────────────────────────────────────────────
|
||||
function onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var satellite = dom.name ? dom.name.value.trim() : "";
|
||||
var noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
|
||||
var bmId = dom.bookmark ? dom.bookmark.value : "";
|
||||
|
||||
if (!satellite) { alert("Please enter a satellite name."); return; }
|
||||
if (isNaN(noradId) || noradId <= 0) { alert("Please enter a valid NORAD catalog number."); return; }
|
||||
if (!bmId) { alert("Please select a bookmark."); return; }
|
||||
|
||||
var minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
|
||||
var prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
|
||||
var centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
|
||||
|
||||
var sat = ensureSatelliteConfig();
|
||||
|
||||
var entryData = {
|
||||
satellite: satellite,
|
||||
norad_id: noradId,
|
||||
bookmark_id: bmId,
|
||||
min_elevation_deg: isNaN(minEl) ? 5 : minEl,
|
||||
priority: isNaN(prio) ? 0 : prio,
|
||||
center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
|
||||
bookmark_ids: [],
|
||||
};
|
||||
|
||||
if (editIdx !== null) {
|
||||
var existing = sat.entries[editIdx];
|
||||
entryData.id = existing ? existing.id : ("sat_" + Date.now().toString(36));
|
||||
sat.entries[editIdx] = entryData;
|
||||
} else {
|
||||
entryData.id = "sat_" + Date.now().toString(36);
|
||||
sat.entries.push(entryData);
|
||||
}
|
||||
|
||||
closeForm();
|
||||
renderEntries();
|
||||
}
|
||||
|
||||
// ── Preset change handler ─────────────────────────────────────────
|
||||
function onPresetChange() {
|
||||
if (!dom.preset || !dom.preset.value) return;
|
||||
var parts = dom.preset.value.split("|");
|
||||
if (dom.name) dom.name.value = parts[0] || "";
|
||||
if (dom.norad) dom.norad.value = parts[1] || "";
|
||||
}
|
||||
|
||||
// ── Wire all events ───────────────────────────────────────────────
|
||||
function wireEvents() {
|
||||
if (dom.enabled) {
|
||||
dom.enabled.addEventListener("change", function () {
|
||||
if (dom.body) dom.body.style.display = dom.enabled.checked ? "" : "none";
|
||||
});
|
||||
}
|
||||
if (dom.addBtn) dom.addBtn.addEventListener("click", function () { openForm(null, null); });
|
||||
if (dom.form) dom.form.addEventListener("submit", onFormSubmit);
|
||||
if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm);
|
||||
if (dom.preset) dom.preset.addEventListener("change", onPresetChange);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────
|
||||
window.satScheduler = {
|
||||
wireEvents: wireEvents,
|
||||
renderSection: renderSection,
|
||||
renderPassStatus: renderPassStatus,
|
||||
collectSatelliteConfig: collectSatelliteConfig,
|
||||
};
|
||||
})();
|
||||
@@ -3,27 +3,46 @@
|
||||
// History view: filterable table of all decoded images
|
||||
// Predictions view: next 24 h passes for ham satellites
|
||||
|
||||
// ── DOM references ──────────────────────────────────────────────────
|
||||
const satStatus = document.getElementById("sat-status");
|
||||
const satLiveView = document.getElementById("sat-live-view");
|
||||
const satHistoryView = document.getElementById("sat-history-view");
|
||||
const satPredictionsView = document.getElementById("sat-predictions-view");
|
||||
const satLiveLatest = document.getElementById("sat-live-latest");
|
||||
const satHistoryList = document.getElementById("sat-history-list");
|
||||
const satHistoryCount = document.getElementById("sat-history-count");
|
||||
const satFilterInput = document.getElementById("sat-filter");
|
||||
const satSortSelect = document.getElementById("sat-sort");
|
||||
const satTypeFilter = document.getElementById("sat-type-filter");
|
||||
const satAptState = document.getElementById("sat-apt-state");
|
||||
const satLrptState = document.getElementById("sat-lrpt-state");
|
||||
// ── DOM references (cached once) ───────────────────────────────────
|
||||
const satDom = {
|
||||
status: document.getElementById("sat-status"),
|
||||
liveView: document.getElementById("sat-live-view"),
|
||||
historyView: document.getElementById("sat-history-view"),
|
||||
predictionsView: document.getElementById("sat-predictions-view"),
|
||||
liveLatest: document.getElementById("sat-live-latest"),
|
||||
historyList: document.getElementById("sat-history-list"),
|
||||
historyCount: document.getElementById("sat-history-count"),
|
||||
filterInput: document.getElementById("sat-filter"),
|
||||
sortSelect: document.getElementById("sat-sort"),
|
||||
typeFilter: document.getElementById("sat-type-filter"),
|
||||
aptState: document.getElementById("sat-apt-state"),
|
||||
lrptState: document.getElementById("sat-lrpt-state"),
|
||||
viewLiveBtn: document.getElementById("sat-view-live"),
|
||||
viewHistoryBtn: document.getElementById("sat-view-history"),
|
||||
viewPredBtn: document.getElementById("sat-view-predictions"),
|
||||
predFilter: document.getElementById("sat-pred-filter"),
|
||||
predMinEl: document.getElementById("sat-pred-min-el"),
|
||||
predCategory: document.getElementById("sat-pred-category"),
|
||||
predCurrentList: document.getElementById("sat-pred-current-list"),
|
||||
predUpcomingList: document.getElementById("sat-pred-list"),
|
||||
predCurrentSec: document.getElementById("sat-pred-current-section"),
|
||||
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
|
||||
predStatus: document.getElementById("sat-pred-status"),
|
||||
};
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────
|
||||
let satImageHistory = [];
|
||||
const SAT_MAX_IMAGES = 100;
|
||||
const SAT_PRED_PAGE_SIZE = 50; // max rows before "Show more"
|
||||
const SAT_PRED_PAGE_SIZE = 50;
|
||||
let satPredShowAll = false;
|
||||
let satFilterText = "";
|
||||
let satActiveView = "live"; // "live" | "history" | "predictions"
|
||||
let satPredData = [];
|
||||
let satPredFilterText = "";
|
||||
let satPredMinEl = 0;
|
||||
let satPredCategory = "all";
|
||||
let satPredSatCount = 0;
|
||||
let satPredCountdownTimer = null;
|
||||
|
||||
// ── UI scheduler helper ─────────────────────────────────────────────
|
||||
function scheduleSatUi(key, job) {
|
||||
@@ -35,23 +54,16 @@ function scheduleSatUi(key, job) {
|
||||
}
|
||||
|
||||
// ── View switching ──────────────────────────────────────────────────
|
||||
const satViewLiveBtn = document.getElementById("sat-view-live");
|
||||
const satViewHistoryBtn = document.getElementById("sat-view-history");
|
||||
const satViewPredictionsBtn = document.getElementById("sat-view-predictions");
|
||||
|
||||
function switchSatView(view) {
|
||||
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
|
||||
satActiveView = view;
|
||||
if (satLiveView) satLiveView.style.display = view === "live" ? "" : "none";
|
||||
if (satHistoryView) satHistoryView.style.display = view === "history" ? "" : "none";
|
||||
if (satPredictionsView) satPredictionsView.style.display = view === "predictions" ? "" : "none";
|
||||
if (satViewLiveBtn) satViewLiveBtn.classList.toggle("sat-view-active", view === "live");
|
||||
if (satViewHistoryBtn) satViewHistoryBtn.classList.toggle("sat-view-active", view === "history");
|
||||
if (satViewPredictionsBtn) satViewPredictionsBtn.classList.toggle("sat-view-active", view === "predictions");
|
||||
// Clear prediction DOM when leaving to reduce node count.
|
||||
if (leavingPredictions) {
|
||||
clearPredictionDom();
|
||||
}
|
||||
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") {
|
||||
@@ -61,39 +73,38 @@ function switchSatView(view) {
|
||||
}
|
||||
|
||||
function clearPredictionDom() {
|
||||
if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; }
|
||||
if (satPredCurrentList) satPredCurrentList.innerHTML = "";
|
||||
if (satPredUpcomingList) satPredUpcomingList.innerHTML = "";
|
||||
stopCountdownTimer();
|
||||
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
|
||||
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
|
||||
}
|
||||
window.clearSatPredictionDom = clearPredictionDom;
|
||||
|
||||
satViewLiveBtn?.addEventListener("click", () => switchSatView("live"));
|
||||
satViewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
|
||||
satViewPredictionsBtn?.addEventListener("click", () => switchSatView("predictions"));
|
||||
satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
|
||||
satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
|
||||
satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
|
||||
|
||||
// ── Live view: decoder state ────────────────────────────────────────
|
||||
// Updated from app.js render() via window.updateSatLiveState
|
||||
let _lastSatAptOn = null, _lastSatLrptOn = null;
|
||||
window.updateSatLiveState = function (update) {
|
||||
if (!satAptState || !satLrptState) return;
|
||||
if (!satDom.aptState || !satDom.lrptState) return;
|
||||
const aptOn = !!update.wxsat_decode_enabled;
|
||||
const lrptOn = !!update.lrpt_decode_enabled;
|
||||
if (aptOn !== _lastSatAptOn) {
|
||||
_lastSatAptOn = aptOn;
|
||||
satAptState.textContent = aptOn ? "Listening" : "Idle";
|
||||
satAptState.className = "sat-live-value " + (aptOn ? "sat-state-listening" : "sat-state-idle");
|
||||
satDom.aptState.textContent = aptOn ? "Listening" : "Idle";
|
||||
satDom.aptState.className = "sat-live-value " + (aptOn ? "sat-state-listening" : "sat-state-idle");
|
||||
}
|
||||
if (lrptOn !== _lastSatLrptOn) {
|
||||
_lastSatLrptOn = lrptOn;
|
||||
satLrptState.textContent = lrptOn ? "Listening" : "Idle";
|
||||
satLrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
||||
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
|
||||
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
|
||||
}
|
||||
};
|
||||
|
||||
function renderSatLatestCard() {
|
||||
if (!satLiveLatest) return;
|
||||
if (!satDom.liveLatest) return;
|
||||
if (satImageHistory.length === 0) {
|
||||
satLiveLatest.innerHTML =
|
||||
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;
|
||||
}
|
||||
@@ -124,19 +135,17 @@ function renderSatLatestCard() {
|
||||
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>`;
|
||||
satLiveLatest.innerHTML = html;
|
||||
satDom.liveLatest.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── History view: table ─────────────────────────────────────────────
|
||||
function getSatFilteredHistory() {
|
||||
let items = satImageHistory;
|
||||
|
||||
// Type filter
|
||||
const typeVal = satTypeFilter ? satTypeFilter.value : "all";
|
||||
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
|
||||
if (typeVal === "apt") items = items.filter((i) => i._decoder === "apt");
|
||||
else if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
|
||||
|
||||
// Text filter
|
||||
if (satFilterText) {
|
||||
items = items.filter((i) => {
|
||||
const haystack = [
|
||||
@@ -145,18 +154,13 @@ function getSatFilteredHistory() {
|
||||
i.channels || "",
|
||||
i.channel_a || "",
|
||||
i.channel_b || "",
|
||||
]
|
||||
.join(" ")
|
||||
.toUpperCase();
|
||||
].join(" ").toUpperCase();
|
||||
return haystack.includes(satFilterText);
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
const sortVal = satSortSelect ? satSortSelect.value : "newest";
|
||||
if (sortVal === "oldest") {
|
||||
items = items.slice().reverse();
|
||||
}
|
||||
const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
|
||||
if (sortVal === "oldest") items = items.slice().reverse();
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -194,18 +198,18 @@ function renderSatHistoryRow(img) {
|
||||
}
|
||||
|
||||
function renderSatHistoryTable() {
|
||||
if (!satHistoryList) return;
|
||||
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]));
|
||||
}
|
||||
satHistoryList.replaceChildren(fragment);
|
||||
satDom.historyList.replaceChildren(fragment);
|
||||
|
||||
if (satHistoryCount) {
|
||||
if (satDom.historyCount) {
|
||||
const total = satImageHistory.length;
|
||||
const shown = items.length;
|
||||
satHistoryCount.textContent =
|
||||
satDom.historyCount.textContent =
|
||||
total === 0
|
||||
? "No images yet"
|
||||
: shown === total
|
||||
@@ -238,7 +242,7 @@ function addSatImage(img, decoder) {
|
||||
|
||||
// ── Server callbacks ────────────────────────────────────────────────
|
||||
window.onServerSatImage = function (msg) {
|
||||
if (satStatus) satStatus.textContent = "Image received (NOAA APT)";
|
||||
if (satDom.status) satDom.status.textContent = "Image received (NOAA APT)";
|
||||
addSatImage(msg, "apt");
|
||||
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
|
||||
window.addSatMapOverlay(msg);
|
||||
@@ -246,7 +250,7 @@ window.onServerSatImage = function (msg) {
|
||||
};
|
||||
|
||||
window.onServerLrptImage = function (msg) {
|
||||
if (satStatus) satStatus.textContent = "Image received (Meteor LRPT)";
|
||||
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
|
||||
addSatImage(msg, "lrpt");
|
||||
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
|
||||
window.addSatMapOverlay(msg);
|
||||
@@ -255,7 +259,7 @@ window.onServerLrptImage = function (msg) {
|
||||
|
||||
window.resetSatHistoryView = function () {
|
||||
satImageHistory = [];
|
||||
if (satHistoryList) satHistoryList.innerHTML = "";
|
||||
if (satDom.historyList) satDom.historyList.innerHTML = "";
|
||||
renderSatLatestCard();
|
||||
renderSatHistoryTable();
|
||||
if (window.clearSatMapOverlays) window.clearSatMapOverlays();
|
||||
@@ -288,13 +292,13 @@ lrptDecodeToggleBtn?.addEventListener("click", async () => {
|
||||
});
|
||||
|
||||
// ── Filter / sort event listeners ───────────────────────────────────
|
||||
satFilterInput?.addEventListener("input", () => {
|
||||
satFilterText = satFilterInput.value.trim().toUpperCase();
|
||||
satDom.filterInput?.addEventListener("input", () => {
|
||||
satFilterText = satDom.filterInput.value.trim().toUpperCase();
|
||||
renderSatHistoryTable();
|
||||
});
|
||||
|
||||
satSortSelect?.addEventListener("change", () => renderSatHistoryTable());
|
||||
satTypeFilter?.addEventListener("change", () => renderSatHistoryTable());
|
||||
satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
|
||||
satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
|
||||
|
||||
// ── Settings: clear history ─────────────────────────────────────────
|
||||
document
|
||||
@@ -309,55 +313,7 @@ document
|
||||
}
|
||||
});
|
||||
|
||||
// ── Predictions view ────────────────────────────────────────────────
|
||||
let satPredData = [];
|
||||
let satPredFilterText = "";
|
||||
let satPredMinEl = 0;
|
||||
let satPredCategory = "all";
|
||||
let satPredSatCount = 0;
|
||||
let satPredCountdownTimer = null;
|
||||
const satPredFilterInput = document.getElementById("sat-pred-filter");
|
||||
const satPredMinElSelect = document.getElementById("sat-pred-min-el");
|
||||
const satPredCategorySelect = document.getElementById("sat-pred-category");
|
||||
const satPredCurrentList = document.getElementById("sat-pred-current-list");
|
||||
const satPredUpcomingList = document.getElementById("sat-pred-list");
|
||||
const satPredCurrentSection = document.getElementById("sat-pred-current-section");
|
||||
const satPredUpcomingSection = document.getElementById("sat-pred-upcoming-section");
|
||||
const satPredStatus = document.getElementById("sat-pred-status");
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
satPredFilterInput?.addEventListener("input", () => {
|
||||
satPredFilterText = satPredFilterInput.value.trim().toUpperCase();
|
||||
applyPredFilters();
|
||||
});
|
||||
|
||||
satPredMinElSelect?.addEventListener("change", () => {
|
||||
satPredMinEl = parseInt(satPredMinElSelect.value, 10) || 0;
|
||||
applyPredFilters();
|
||||
});
|
||||
|
||||
satPredCategorySelect?.addEventListener("change", () => {
|
||||
satPredCategory = satPredCategorySelect.value;
|
||||
applyPredFilters();
|
||||
});
|
||||
|
||||
// ── Predictions: helpers ────────────────────────────────────────────
|
||||
function azToCardinal(deg) {
|
||||
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
||||
return dirs[Math.round(deg / 45) % 8];
|
||||
@@ -367,9 +323,7 @@ function formatPredTime(ms) {
|
||||
const d = new Date(ms);
|
||||
const now = new Date();
|
||||
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 mm = String(d.getUTCMinutes()).padStart(2, "0");
|
||||
return `${day}${hh}:${mm}`;
|
||||
@@ -387,31 +341,126 @@ function formatCountdown(ms) {
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function renderSatPredictions(passes, error) {
|
||||
const currentList = satPredCurrentList;
|
||||
const upcomingList = satPredUpcomingList;
|
||||
const currentSection = satPredCurrentSection;
|
||||
const upcomingSection = satPredUpcomingSection;
|
||||
const status = satPredStatus;
|
||||
function elevationClass(deg) {
|
||||
if (deg >= 45) return "sat-pred-el-high";
|
||||
if (deg >= 10) return "sat-pred-el-mid";
|
||||
return "sat-pred-el-low";
|
||||
}
|
||||
|
||||
// Stop any previous countdown timer
|
||||
if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; }
|
||||
// ── Predictions: countdown timer management ─────────────────────────
|
||||
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;
|
||||
}
|
||||
const n = Date.now();
|
||||
let anyActive = false;
|
||||
for (const el of countdownEls) {
|
||||
const los = parseInt(el.dataset.los, 10);
|
||||
const rem = los - n;
|
||||
if (rem > 0) {
|
||||
el.textContent = formatCountdown(rem);
|
||||
anyActive = true;
|
||||
} else {
|
||||
el.textContent = "0:00";
|
||||
}
|
||||
}
|
||||
if (!anyActive) {
|
||||
stopCountdownTimer();
|
||||
renderSatPredictions(getFilteredPredictions());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ── Predictions: row builders ───────────────────────────────────────
|
||||
function buildCurrentPassRow(pass, now) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-pred-row-current";
|
||||
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${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)}\u00B0</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)} \u2192 ${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)}\u00B0</span>`,
|
||||
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
|
||||
`<span class="sat-pred-col-dir">${dir}</span>`,
|
||||
].join("");
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Predictions: filter state ───────────────────────────────────────
|
||||
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();
|
||||
});
|
||||
|
||||
// ── Predictions: main render ────────────────────────────────────────
|
||||
function renderSatPredictions(passes, error) {
|
||||
stopCountdownTimer();
|
||||
|
||||
if (error) {
|
||||
if (currentList) currentList.innerHTML = "";
|
||||
if (upcomingList) upcomingList.innerHTML = "";
|
||||
if (currentSection) currentSection.style.display = "none";
|
||||
if (upcomingSection) upcomingSection.style.display = "none";
|
||||
if (status) status.textContent = 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 (currentList) currentList.innerHTML = "";
|
||||
if (upcomingList) upcomingList.innerHTML = "";
|
||||
if (currentSection) currentSection.style.display = "none";
|
||||
if (upcomingSection) upcomingSection.style.display = "none";
|
||||
if (status) status.textContent = "No passes found in the next 24 hours.";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -420,61 +469,25 @@ function renderSatPredictions(passes, error) {
|
||||
const upcoming = passes.filter((p) => p.aos_ms > now);
|
||||
|
||||
// ── Current passes ──
|
||||
if (currentSection) currentSection.style.display = current.length > 0 ? "" : "none";
|
||||
if (currentList) {
|
||||
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
|
||||
if (satDom.predCurrentList) {
|
||||
if (current.length === 0) {
|
||||
currentList.innerHTML = "";
|
||||
satDom.predCurrentList.innerHTML = "";
|
||||
} else {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const pass of current) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-pred-row-current";
|
||||
const elClass = pass.max_elevation_deg >= 45
|
||||
? "sat-pred-el-high"
|
||||
: pass.max_elevation_deg >= 10
|
||||
? "sat-pred-el-mid"
|
||||
: "sat-pred-el-low";
|
||||
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 ${elClass}">${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("");
|
||||
frag.appendChild(row);
|
||||
}
|
||||
currentList.replaceChildren(frag);
|
||||
for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
|
||||
satDom.predCurrentList.replaceChildren(frag);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Upcoming passes (capped to reduce DOM node count) ──
|
||||
// ── Upcoming passes ──
|
||||
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
|
||||
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
|
||||
const hiddenCount = upcoming.length - visibleUpcoming.length;
|
||||
if (upcomingSection) upcomingSection.style.display = upcoming.length > 0 ? "" : "none";
|
||||
if (upcomingList) {
|
||||
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
|
||||
if (satDom.predUpcomingList) {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const pass of visibleUpcoming) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "sat-pred-row";
|
||||
const elClass = pass.max_elevation_deg >= 45
|
||||
? "sat-pred-el-high"
|
||||
: pass.max_elevation_deg >= 10
|
||||
? "sat-pred-el-mid"
|
||||
: "sat-pred-el-low";
|
||||
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 ${elClass}">${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("");
|
||||
frag.appendChild(row);
|
||||
}
|
||||
for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
|
||||
if (hiddenCount > 0) {
|
||||
const moreRow = document.createElement("div");
|
||||
moreRow.className = "sat-pred-row";
|
||||
@@ -487,54 +500,27 @@ function renderSatPredictions(passes, error) {
|
||||
});
|
||||
frag.appendChild(moreRow);
|
||||
}
|
||||
upcomingList.replaceChildren(frag);
|
||||
satDom.predUpcomingList.replaceChildren(frag);
|
||||
}
|
||||
|
||||
// ── Status ──
|
||||
if (status) {
|
||||
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
|
||||
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
|
||||
status.textContent = text;
|
||||
if (satDom.predStatus) {
|
||||
let text = `${current.length} active \u00B7 ${upcoming.length} upcoming \u00B7 times in UTC`;
|
||||
if (satPredSatCount > 0) text += ` \u00B7 ${satPredSatCount} satellites tracked`;
|
||||
satDom.predStatus.textContent = text;
|
||||
}
|
||||
|
||||
// ── Countdown timer: update "time left" every second ──
|
||||
// Only run when predictions view is actually visible.
|
||||
// ── Countdown timer ──
|
||||
if (current.length > 0 && satActiveView === "predictions") {
|
||||
const countdownEls = currentList ? currentList.querySelectorAll(".sat-pred-col-countdown") : [];
|
||||
if (countdownEls.length > 0) {
|
||||
satPredCountdownTimer = setInterval(() => {
|
||||
// Pause timer if predictions view was hidden (e.g. switched tabs).
|
||||
if (satActiveView !== "predictions") {
|
||||
clearInterval(satPredCountdownTimer);
|
||||
satPredCountdownTimer = null;
|
||||
return;
|
||||
}
|
||||
const n = Date.now();
|
||||
let anyActive = false;
|
||||
for (const el of countdownEls) {
|
||||
const los = parseInt(el.dataset.los, 10);
|
||||
const rem = los - n;
|
||||
if (rem > 0) {
|
||||
el.textContent = formatCountdown(rem);
|
||||
anyActive = true;
|
||||
} else {
|
||||
el.textContent = "0:00";
|
||||
}
|
||||
}
|
||||
if (!anyActive) {
|
||||
clearInterval(satPredCountdownTimer);
|
||||
satPredCountdownTimer = null;
|
||||
renderSatPredictions(getFilteredPredictions());
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
startCountdownTimer(satDom.predCurrentList);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Predictions: data loading ───────────────────────────────────────
|
||||
async function loadSatPredictions() {
|
||||
if (satPredStatus) satPredStatus.textContent = "Loading predictions\u2026";
|
||||
if (satPredCurrentList) satPredCurrentList.innerHTML = "";
|
||||
if (satPredUpcomingList) satPredUpcomingList.innerHTML = "";
|
||||
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions\u2026";
|
||||
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}`);
|
||||
@@ -554,11 +540,9 @@ async function loadSatPredictions() {
|
||||
|
||||
// ── Navigate to map centered on satellite image bounds ──────────────
|
||||
window.satShowOnMap = function (south, west, north, east) {
|
||||
// Enable sat filter if not active
|
||||
if (typeof window.enableMapSourceFilter === "function") {
|
||||
window.enableMapSourceFilter("sat");
|
||||
}
|
||||
// Navigate to the center of the image bounds
|
||||
const lat = (south + north) / 2;
|
||||
const lon = (west + east) / 2;
|
||||
if (window.navigateToAprsMap) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
let interleaveTicker = null;
|
||||
let schedulerStepPending = false;
|
||||
let schEntryEditIdx = null; // null = adding, number = editing that index
|
||||
let schSatEditIdx = null; // null = adding, number = editing satellite entry
|
||||
// Satellite entry editing state moved to sat-scheduler.js
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Init
|
||||
@@ -866,225 +866,30 @@
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Satellite overlay
|
||||
// Satellite overlay (delegated to sat-scheduler.js)
|
||||
// -------------------------------------------------------------------------
|
||||
function getSatelliteEntries() {
|
||||
return (currentConfig && currentConfig.satellites && Array.isArray(currentConfig.satellites.entries))
|
||||
? currentConfig.satellites.entries
|
||||
: [];
|
||||
}
|
||||
|
||||
function ensureSatelliteConfig() {
|
||||
if (!currentConfig) currentConfig = { remote: currentRigId, mode: "disabled", entries: [] };
|
||||
if (!currentConfig.satellites) currentConfig.satellites = { enabled: false, pretune_secs: 60, entries: [] };
|
||||
if (!currentConfig.satellites.entries) currentConfig.satellites.entries = [];
|
||||
return currentConfig.satellites;
|
||||
}
|
||||
|
||||
function collectSatelliteConfig() {
|
||||
const enabledEl = document.getElementById("scheduler-sat-enabled");
|
||||
const pretuneEl = document.getElementById("scheduler-sat-pretune");
|
||||
const enabled = enabledEl ? enabledEl.checked : false;
|
||||
const pretune = pretuneEl ? parseInt(pretuneEl.value, 10) : 60;
|
||||
return {
|
||||
enabled: enabled,
|
||||
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
|
||||
entries: getSatelliteEntries(),
|
||||
};
|
||||
}
|
||||
|
||||
function renderSatelliteSection() {
|
||||
const satCfg = (currentConfig && currentConfig.satellites) || {};
|
||||
const enabled = !!satCfg.enabled;
|
||||
const enabledEl = document.getElementById("scheduler-sat-enabled");
|
||||
if (enabledEl) enabledEl.checked = enabled;
|
||||
|
||||
const pretuneEl = document.getElementById("scheduler-sat-pretune");
|
||||
if (pretuneEl) pretuneEl.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60;
|
||||
|
||||
const bodyEl = document.getElementById("scheduler-sat-body");
|
||||
if (bodyEl) bodyEl.style.display = enabled ? "" : "none";
|
||||
|
||||
renderSatelliteEntries();
|
||||
renderSatPassStatus();
|
||||
}
|
||||
|
||||
function renderSatelliteEntries() {
|
||||
const tbody = document.getElementById("scheduler-sat-tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = "";
|
||||
const entries = getSatelliteEntries();
|
||||
entries.forEach(function (entry, idx) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML =
|
||||
"<td>" + escHtml(entry.satellite || "") + "</td>" +
|
||||
"<td>" + (entry.norad_id || "") + "</td>" +
|
||||
"<td>" + escHtml(bmName(entry.bookmark_id)) + "</td>" +
|
||||
"<td>" + (entry.min_elevation_deg != null ? entry.min_elevation_deg + "\u00B0" : "5\u00B0") + "</td>" +
|
||||
"<td>" + (entry.priority || 0) + "</td>" +
|
||||
'<td>' +
|
||||
'<button class="sch-write sch-sat-edit-btn" data-idx="' + idx + '" type="button">Edit</button>' +
|
||||
'<button class="sch-write sch-sat-remove-btn" data-idx="' + idx + '" type="button">Remove</button>' +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".sch-sat-edit-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const i = parseInt(btn.dataset.idx, 10);
|
||||
const entry = getSatelliteEntries()[i];
|
||||
if (entry) schOpenSatForm(entry, i);
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".sch-sat-remove-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
removeSatEntry(parseInt(btn.dataset.idx, 10));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeSatEntry(idx) {
|
||||
const sat = ensureSatelliteConfig();
|
||||
sat.entries.splice(idx, 1);
|
||||
renderSatelliteEntries();
|
||||
}
|
||||
|
||||
function schOpenSatForm(entry, idx) {
|
||||
schSatEditIdx = (idx != null) ? idx : null;
|
||||
|
||||
const titleEl = document.getElementById("sch-sat-form-title");
|
||||
if (titleEl) titleEl.textContent = entry ? "Edit Satellite" : "Add Satellite";
|
||||
|
||||
const presetEl = document.getElementById("scheduler-sat-preset");
|
||||
const nameEl = document.getElementById("scheduler-sat-name");
|
||||
const noradEl = document.getElementById("scheduler-sat-norad");
|
||||
const bmEl = document.getElementById("scheduler-sat-bookmark");
|
||||
const minElEl = document.getElementById("scheduler-sat-min-el");
|
||||
const prioEl = document.getElementById("scheduler-sat-priority");
|
||||
const centerHzEl = document.getElementById("scheduler-sat-center-hz");
|
||||
|
||||
if (presetEl) presetEl.value = "";
|
||||
if (nameEl) nameEl.value = entry ? (entry.satellite || "") : "";
|
||||
if (noradEl) noradEl.value = entry ? (entry.norad_id || "") : "";
|
||||
if (bmEl) bmEl.value = entry ? (entry.bookmark_id || "") : "";
|
||||
if (minElEl) minElEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5;
|
||||
if (prioEl) prioEl.value = entry && entry.priority != null ? entry.priority : 0;
|
||||
if (centerHzEl) centerHzEl.value = entry && entry.center_hz ? entry.center_hz : "";
|
||||
|
||||
// Populate bookmark dropdown
|
||||
renderBookmarkSelect("scheduler-sat-bookmark", entry ? entry.bookmark_id : null);
|
||||
|
||||
const wrap = document.getElementById("sch-sat-form-wrap");
|
||||
if (wrap) {
|
||||
wrap.style.display = "flex";
|
||||
if (nameEl) nameEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function schCloseSatForm() {
|
||||
const wrap = document.getElementById("sch-sat-form-wrap");
|
||||
if (wrap) wrap.style.display = "none";
|
||||
schSatEditIdx = null;
|
||||
}
|
||||
|
||||
function schSatFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const nameEl = document.getElementById("scheduler-sat-name");
|
||||
const noradEl = document.getElementById("scheduler-sat-norad");
|
||||
const bmEl = document.getElementById("scheduler-sat-bookmark");
|
||||
const minElEl = document.getElementById("scheduler-sat-min-el");
|
||||
const prioEl = document.getElementById("scheduler-sat-priority");
|
||||
const centerHzEl = document.getElementById("scheduler-sat-center-hz");
|
||||
|
||||
const satellite = nameEl ? nameEl.value.trim() : "";
|
||||
const noradId = noradEl ? parseInt(noradEl.value, 10) : NaN;
|
||||
const bmId = bmEl ? bmEl.value : "";
|
||||
|
||||
if (!satellite) { alert("Please enter a satellite name."); return; }
|
||||
if (isNaN(noradId) || noradId <= 0) { alert("Please enter a valid NORAD catalog number."); return; }
|
||||
if (!bmId) { alert("Please select a bookmark."); return; }
|
||||
|
||||
const minEl = minElEl ? parseFloat(minElEl.value) : 5;
|
||||
const prio = prioEl ? parseInt(prioEl.value, 10) : 0;
|
||||
const centerHzRaw = centerHzEl ? parseInt(centerHzEl.value, 10) : NaN;
|
||||
|
||||
const sat = ensureSatelliteConfig();
|
||||
|
||||
const entryData = {
|
||||
satellite: satellite,
|
||||
norad_id: noradId,
|
||||
bookmark_id: bmId,
|
||||
min_elevation_deg: isNaN(minEl) ? 5 : minEl,
|
||||
priority: isNaN(prio) ? 0 : prio,
|
||||
center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
|
||||
bookmark_ids: [],
|
||||
};
|
||||
|
||||
if (schSatEditIdx !== null) {
|
||||
const existing = sat.entries[schSatEditIdx];
|
||||
entryData.id = existing ? existing.id : ("sat_" + Date.now().toString(36));
|
||||
sat.entries[schSatEditIdx] = entryData;
|
||||
} else {
|
||||
entryData.id = "sat_" + Date.now().toString(36);
|
||||
sat.entries.push(entryData);
|
||||
}
|
||||
|
||||
schCloseSatForm();
|
||||
renderSatelliteEntries();
|
||||
}
|
||||
|
||||
function wireSatPresetChange() {
|
||||
const presetEl = document.getElementById("scheduler-sat-preset");
|
||||
if (!presetEl || presetEl._wired) return;
|
||||
presetEl._wired = true;
|
||||
presetEl.addEventListener("change", function () {
|
||||
if (!presetEl.value) return;
|
||||
const parts = presetEl.value.split("|");
|
||||
const nameEl = document.getElementById("scheduler-sat-name");
|
||||
const noradEl = document.getElementById("scheduler-sat-norad");
|
||||
if (nameEl) nameEl.value = parts[0] || "";
|
||||
if (noradEl) noradEl.value = parts[1] || "";
|
||||
});
|
||||
if (window.satScheduler) window.satScheduler.renderSection();
|
||||
}
|
||||
|
||||
function renderSatPassStatus() {
|
||||
const el = document.getElementById("scheduler-sat-pass-status");
|
||||
if (!el) return;
|
||||
const entries = getSatelliteEntries();
|
||||
if (entries.length === 0) {
|
||||
el.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
// Show active satellite from status if available.
|
||||
if (currentSchedulerStatus && currentSchedulerStatus.active_satellite) {
|
||||
el.innerHTML =
|
||||
'<span class="sch-sat-active-badge">PASS ACTIVE: ' +
|
||||
escHtml(currentSchedulerStatus.active_satellite) +
|
||||
'</span>';
|
||||
} else {
|
||||
el.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem;">No satellite pass active. Predictions available in the SAT tab.</span>';
|
||||
}
|
||||
if (window.satScheduler) window.satScheduler.renderPassStatus();
|
||||
}
|
||||
|
||||
function collectSatelliteConfig() {
|
||||
return window.satScheduler
|
||||
? window.satScheduler.collectSatelliteConfig()
|
||||
: { enabled: false, pretune_secs: 60, entries: [] };
|
||||
}
|
||||
|
||||
function wireSatelliteEvents() {
|
||||
const enabledEl = document.getElementById("scheduler-sat-enabled");
|
||||
if (enabledEl) {
|
||||
enabledEl.addEventListener("change", function () {
|
||||
const bodyEl = document.getElementById("scheduler-sat-body");
|
||||
if (bodyEl) bodyEl.style.display = enabledEl.checked ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
const addBtn = document.getElementById("scheduler-sat-add-btn");
|
||||
if (addBtn) addBtn.addEventListener("click", function () { schOpenSatForm(null, null); });
|
||||
|
||||
const satForm = document.getElementById("sch-sat-form");
|
||||
if (satForm) satForm.addEventListener("submit", schSatFormSubmit);
|
||||
|
||||
const cancelBtn = document.getElementById("sch-sat-form-cancel");
|
||||
if (cancelBtn) cancelBtn.addEventListener("click", schCloseSatForm);
|
||||
|
||||
wireSatPresetChange();
|
||||
// Expose bridge for sat-scheduler.js to access shared state.
|
||||
window.schedulerBridge = {
|
||||
getConfig: function () { return currentConfig; },
|
||||
getStatus: function () { return currentSchedulerStatus; },
|
||||
getBookmarks: function () { return bookmarkList; },
|
||||
};
|
||||
if (window.satScheduler) window.satScheduler.wireEvents();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -1689,6 +1689,7 @@ define_gz_cache!(gz_cw_js, status::CW_JS, "cw.js");
|
||||
define_gz_cache!(gz_sat_js, status::SAT_JS, "sat.js");
|
||||
define_gz_cache!(gz_bookmarks_js, status::BOOKMARKS_JS, "bookmarks.js");
|
||||
define_gz_cache!(gz_scheduler_js, status::SCHEDULER_JS, "scheduler.js");
|
||||
define_gz_cache!(gz_sat_scheduler_js, status::SAT_SCHEDULER_JS, "sat-scheduler.js");
|
||||
define_gz_cache!(gz_background_decode_js, status::BACKGROUND_DECODE_JS, "background-decode.js");
|
||||
define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js");
|
||||
|
||||
@@ -2249,6 +2250,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(sat_js)
|
||||
.service(bookmarks_js)
|
||||
.service(scheduler_js)
|
||||
.service(sat_scheduler_js)
|
||||
.service(background_decode_js)
|
||||
.service(vchan_js)
|
||||
// Virtual channels
|
||||
@@ -2421,6 +2423,12 @@ async fn scheduler_js(req: HttpRequest) -> impl Responder {
|
||||
static_asset_response(&req, "application/javascript; charset=utf-8", &c.gz, &c.etag)
|
||||
}
|
||||
|
||||
#[get("/sat-scheduler.js")]
|
||||
async fn sat_scheduler_js(req: HttpRequest) -> impl Responder {
|
||||
let c = gz_sat_scheduler_js();
|
||||
static_asset_response(&req, "application/javascript; charset=utf-8", &c.gz, &c.etag)
|
||||
}
|
||||
|
||||
#[get("/background-decode.js")]
|
||||
async fn background_decode_js(req: HttpRequest) -> impl Responder {
|
||||
let c = gz_background_decode_js();
|
||||
|
||||
@@ -27,6 +27,7 @@ pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
|
||||
pub const SAT_JS: &str = include_str!("../assets/web/plugins/sat.js");
|
||||
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
||||
pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js");
|
||||
pub const SAT_SCHEDULER_JS: &str = include_str!("../assets/web/plugins/sat-scheduler.js");
|
||||
pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js");
|
||||
pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user