315 lines
15 KiB
JavaScript
315 lines
15 KiB
JavaScript
import {
|
|
aprsAgeText,
|
|
aprsCategoryLabel,
|
|
aprsHexBytes,
|
|
aprsPacketCategory,
|
|
collapseAprsDuplicates,
|
|
normalizeAprsPacket,
|
|
renderAprsInfo,
|
|
renderLocalAprsSymbol
|
|
} from "./chunk-M2I6DH4X.js";
|
|
|
|
// src/plugins/aprs.ts
|
|
var aprsWindow = window;
|
|
var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
var showAprsHint = (message, durationMs) => {
|
|
aprsWindow.showHint?.(message, durationMs);
|
|
};
|
|
var aprsStatus = document.getElementById("aprs-status");
|
|
var aprsPacketsEl = document.getElementById("aprs-packets");
|
|
var aprsFilterInput = document.getElementById("aprs-filter");
|
|
var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
|
|
var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
|
|
var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
|
|
var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
|
|
var aprsTotalCountEl = document.getElementById("aprs-total-count");
|
|
var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
|
|
var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
|
|
var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
|
|
var aprsFilterText = "";
|
|
var aprsPacketHistory = [];
|
|
var aprsBarDismissedAtMs = 0;
|
|
var aprsOnlyPos = false;
|
|
var aprsHideCrc = false;
|
|
var aprsCollapseDup = false;
|
|
var aprsTypeFilter = "all";
|
|
function currentAprsHistoryRetentionMs() {
|
|
return typeof aprsWindow.getDecodeHistoryRetentionMs === "function" ? aprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
|
|
}
|
|
function pruneAprsPacketHistory() {
|
|
const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
|
|
aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
|
|
}
|
|
function scheduleAprsUi(key, job) {
|
|
if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
|
|
aprsWindow.trxScheduleUiFrameJob(key, job);
|
|
return;
|
|
}
|
|
job();
|
|
}
|
|
function scheduleAprsHistoryRender() {
|
|
scheduleAprsUi("aprs-history", () => {
|
|
renderAprsHistory();
|
|
});
|
|
}
|
|
function scheduleAprsBarUpdate() {
|
|
scheduleAprsUi("aprs-bar", () => {
|
|
updateAprsBar();
|
|
});
|
|
}
|
|
function aprsDistanceText(pkt) {
|
|
if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
|
|
const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon);
|
|
if (!Number.isFinite(distKm)) return "";
|
|
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
|
|
return `${distKm.toFixed(1)} km from TRX`;
|
|
}
|
|
function aprsFilterMatch(pkt) {
|
|
if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
|
|
if (aprsHideCrc && !pkt.crcOk) return false;
|
|
if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
|
|
if (!aprsFilterText) return true;
|
|
const haystack = [
|
|
pkt.srcCall,
|
|
pkt.destCall,
|
|
pkt.path,
|
|
pkt.info,
|
|
pkt.type,
|
|
pkt.lat != null ? pkt.lat.toFixed(4) : "",
|
|
pkt.lon != null ? pkt.lon.toFixed(4) : "",
|
|
aprsPacketCategory(pkt)
|
|
].filter(Boolean).join(" ").toUpperCase();
|
|
return haystack.includes(aprsFilterText);
|
|
}
|
|
function aprsVisiblePackets() {
|
|
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
|
|
return packets.filter(aprsFilterMatch);
|
|
}
|
|
function updateAprsSummary() {
|
|
const visible = aprsVisiblePackets();
|
|
if (aprsTotalCountEl) {
|
|
aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
|
|
}
|
|
if (aprsVisibleCountEl) {
|
|
aprsVisibleCountEl.textContent = `${visible.length} shown`;
|
|
}
|
|
if (aprsLatestSeenEl) {
|
|
const latest = aprsPacketHistory[0];
|
|
if (!latest) {
|
|
aprsLatestSeenEl.textContent = "No packets yet";
|
|
} else {
|
|
aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
|
|
}
|
|
}
|
|
}
|
|
function updateAprsChipState() {
|
|
document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
|
|
});
|
|
aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
|
|
aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
|
|
aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
|
|
}
|
|
function renderAprsRow(pkt, isFresh) {
|
|
const row = document.createElement("div");
|
|
row.className = "aprs-packet";
|
|
if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
|
|
if (isFresh) row.classList.add("aprs-packet-new");
|
|
const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
const age = aprsAgeText(pkt._tsMs);
|
|
const category = aprsPacketCategory(pkt);
|
|
const categoryLabel = aprsCategoryLabel(category);
|
|
const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
|
|
const pathBadge = pkt.path ? `<span class="aprs-badge">${escapeAprsHtml(pkt.path)}</span>` : "";
|
|
const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
|
|
const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
|
|
const posLink = pkt.lat != null && pkt.lon != null ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>` : "";
|
|
const distance = aprsDistanceText(pkt);
|
|
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
|
|
row.innerHTML = `<div class="aprs-row-head"><span class="aprs-time">${ts}</span>` + symbolHtml + `<span class="aprs-call">${escapeAprsHtml(pkt.srcCall ?? "")}</span><span>>${escapeAprsHtml(pkt.destCall || "")}</span><span class="${categoryClass}">${escapeAprsHtml(categoryLabel)}</span>` + pathBadge + crcBadge + `</div><div class="aprs-row-meta"><span class="aprs-meta-text">${escapeAprsHtml(age)}</span>` + (distance ? `<span class="aprs-meta-text">${escapeAprsHtml(distance)}</span>` : "") + `<span class="aprs-meta-text">${escapeAprsHtml(pkt.type || "--")}</span></div><div class="aprs-row-detail"><span title="${escapeAprsHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` + (posLink ? `<span>${posLink}</span>` : "") + `</div><div class="aprs-row-actions">` + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div><details class="aprs-details"><summary>Details</summary><div class="aprs-details-grid"><span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.srcCall || "--")}</span><span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.destCall || "--")}</span><span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.type || "--")}</span><span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.path || "--")}</span><span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeAprsHtml(age)}</span><span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span><span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span><span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.info || "--")}</span><span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}</span></div></details>`;
|
|
row.querySelectorAll("[data-aprs-map]").forEach((el) => {
|
|
el.addEventListener("click", (evt) => {
|
|
evt.preventDefault();
|
|
const raw = el.dataset.aprsMap ?? "";
|
|
const [lat, lon] = raw.split(",").map(Number);
|
|
if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
|
|
aprsWindow.navigateToAprsMap(lat, lon);
|
|
}
|
|
});
|
|
});
|
|
const copyBtn = row.querySelector("[data-aprs-copy]");
|
|
if (copyBtn) {
|
|
copyBtn.addEventListener("click", () => {
|
|
void (async () => {
|
|
const raw = copyBtn.dataset.aprsCopy ?? "";
|
|
try {
|
|
const clipboard = Reflect.get(navigator, "clipboard");
|
|
if (clipboard) {
|
|
await clipboard.writeText(raw);
|
|
showAprsHint("Coordinates copied", 1200);
|
|
}
|
|
} catch {
|
|
showAprsHint("Copy failed", 1500);
|
|
}
|
|
})();
|
|
});
|
|
}
|
|
return row;
|
|
}
|
|
function renderAprsHistory() {
|
|
pruneAprsPacketHistory();
|
|
if (!aprsPacketsEl) {
|
|
updateAprsSummary();
|
|
updateAprsChipState();
|
|
return;
|
|
}
|
|
const visible = aprsVisiblePackets();
|
|
const fragment = document.createDocumentFragment();
|
|
for (const [index, packet] of visible.entries()) {
|
|
fragment.appendChild(renderAprsRow(packet, index === 0));
|
|
}
|
|
aprsPacketsEl.replaceChildren(fragment);
|
|
updateAprsSummary();
|
|
updateAprsChipState();
|
|
}
|
|
function updateAprsBar() {
|
|
if (!aprsBarOverlay) return;
|
|
const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
|
|
const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
|
|
const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
|
|
const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
|
|
const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
|
|
if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
|
|
aprsBarOverlay.style.display = "none";
|
|
aprsBarOverlay.innerHTML = "";
|
|
return;
|
|
}
|
|
let html = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">APRS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-actions"><span class="aprs-bar-window">Last 15 minutes</span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAprsBar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearAprsBar();}" aria-label="Clear APRS overlay">Clear</span></span><button class="aprs-bar-close" type="button" onclick="window.closeAprsBar()" aria-label="Close APRS overlay">×</button></span></div>`;
|
|
for (const pkt of frames) {
|
|
const ts = pkt._ts ? `<span class="aprs-bar-time">${pkt._ts}</span>` : "";
|
|
const call = `<span class="aprs-bar-call">${escapeAprsHtml(pkt.srcCall ?? "")}</span>`;
|
|
const dest = escapeAprsHtml(pkt.destCall || "");
|
|
const info = escapeAprsHtml(pkt.info || "");
|
|
const pin = pkt.lat != null && pkt.lon != null ? `<button class="aprs-bar-pin" title="${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}" onclick="window.navigateToAprsMap(${pkt.lat},${pkt.lon})">📍</button>` : "";
|
|
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}${pin}${call}>${dest}: ${info}</div></div>`;
|
|
}
|
|
aprsBarOverlay.innerHTML = html;
|
|
aprsBarOverlay.style.display = "flex";
|
|
}
|
|
aprsWindow.updateAprsBar = updateAprsBar;
|
|
aprsWindow.clearAprsBar = function() {
|
|
resetAprsHistoryView();
|
|
};
|
|
aprsWindow.closeAprsBar = function() {
|
|
aprsBarDismissedAtMs = Date.now();
|
|
if (aprsBarOverlay) {
|
|
aprsBarOverlay.style.display = "none";
|
|
aprsBarOverlay.innerHTML = "";
|
|
}
|
|
};
|
|
function resetAprsHistoryView() {
|
|
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
|
|
aprsPacketHistory = [];
|
|
updateAprsBar();
|
|
renderAprsHistory();
|
|
aprsWindow.clearMapMarkersByType?.("aprs");
|
|
}
|
|
function pruneAprsHistoryView() {
|
|
pruneAprsPacketHistory();
|
|
updateAprsBar();
|
|
renderAprsHistory();
|
|
}
|
|
function addAprsPacket(pkt) {
|
|
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
|
pkt._tsMs = tsMs;
|
|
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
aprsPacketHistory.unshift(pkt);
|
|
pruneAprsPacketHistory();
|
|
if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
|
|
aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
|
|
}
|
|
if (pkt.crcOk) scheduleAprsBarUpdate();
|
|
scheduleAprsHistoryRender();
|
|
}
|
|
function normalizeServerAprsPacket(pkt) {
|
|
return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
|
|
}
|
|
function onServerAprsBatch(packets) {
|
|
if (!Array.isArray(packets) || packets.length === 0) return;
|
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
|
const normalized = [];
|
|
let hasCrcOk = false;
|
|
for (const pkt of packets) {
|
|
const next = normalizeServerAprsPacket(pkt);
|
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
|
next._tsMs = tsMs;
|
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
if (next.lat != null && next.lon != null && aprsWindow.aprsMapAddStation) {
|
|
aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
|
|
}
|
|
if (next.crcOk) hasCrcOk = true;
|
|
normalized.push(next);
|
|
}
|
|
normalized.reverse();
|
|
aprsPacketHistory = normalized.concat(aprsPacketHistory);
|
|
pruneAprsPacketHistory();
|
|
if (hasCrcOk) scheduleAprsBarUpdate();
|
|
scheduleAprsHistoryRender();
|
|
}
|
|
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
|
|
void (async () => {
|
|
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
|
|
try {
|
|
await aprsWindow.postPath?.("/clear_aprs_decode");
|
|
resetAprsHistoryView();
|
|
} catch (e) {
|
|
console.error("APRS history clear failed", e);
|
|
}
|
|
})();
|
|
});
|
|
if (aprsOnlyPosBtn) {
|
|
aprsOnlyPosBtn.addEventListener("click", () => {
|
|
aprsOnlyPos = !aprsOnlyPos;
|
|
renderAprsHistory();
|
|
});
|
|
}
|
|
if (aprsHideCrcBtn) {
|
|
aprsHideCrcBtn.addEventListener("click", () => {
|
|
aprsHideCrc = !aprsHideCrc;
|
|
renderAprsHistory();
|
|
});
|
|
}
|
|
if (aprsCollapseDupBtn) {
|
|
aprsCollapseDupBtn.addEventListener("click", () => {
|
|
aprsCollapseDup = !aprsCollapseDup;
|
|
renderAprsHistory();
|
|
});
|
|
}
|
|
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
|
|
const btn = document.getElementById(`aprs-type-${type}`);
|
|
if (!btn) return;
|
|
btn.addEventListener("click", () => {
|
|
aprsTypeFilter = type;
|
|
renderAprsHistory();
|
|
});
|
|
});
|
|
if (aprsFilterInput) {
|
|
aprsFilterInput.addEventListener("input", () => {
|
|
aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
|
|
renderAprsHistory();
|
|
});
|
|
}
|
|
function onServerAprs(pkt) {
|
|
if (aprsStatus) aprsStatus.textContent = "Receiving";
|
|
addAprsPacket(normalizeServerAprsPacket(pkt));
|
|
}
|
|
renderAprsHistory();
|
|
window.trxPluginRuntime.registerDecoder({
|
|
id: "aprs",
|
|
onMessage: onServerAprs,
|
|
onBatch: onServerAprsBatch,
|
|
restore: onServerAprsBatch,
|
|
reset: resetAprsHistoryView,
|
|
prune: pruneAprsHistoryView
|
|
});
|