From 4f35be539f317c613bfd8692f45ff7860cc62fb0 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Fri, 13 Feb 2026 02:11:31 +0100 Subject: [PATCH] [feat](trx-frontend-http): improve locator map plotting and themed filters Refine map plotting and filter UX in HTTP frontend plugins.\n\n- support plotting multiple locator squares from FT8/WSPR messages\n- show locator lists in popup content as newline-separated entries\n- add WSPR map layer filter toggle and marker typing\n- style filter controls for strong dark/light mode contrast\n- keep themed behavior aligned with map and control updates\n\nCo-authored-by: OpenAI Codex Signed-off-by: Stanislaw Grams --- .../trx-frontend-http/assets/web/app.js | 51 ++++++++++++++----- .../trx-frontend-http/assets/web/index.html | 1 + .../assets/web/plugins/ft8.js | 17 ++++--- .../assets/web/plugins/wspr.js | 21 +++++++- .../trx-frontend-http/assets/web/style.css | 27 +++++++++- 5 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 1930b6e..cbf9aa7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -990,7 +990,7 @@ let aprsMapBaseLayer = null; let aprsMapReceiverMarker = null; const stationMarkers = new Map(); const mapMarkers = new Set(); -const mapFilter = { aprs: true, ft8: true }; +const mapFilter = { aprs: true, ft8: true, wspr: true }; function mapTileSpecForTheme(theme) { if (theme === "dark") { @@ -1044,6 +1044,7 @@ function initAprsMap() { const aprsFilter = document.getElementById("map-filter-aprs"); const ft8Filter = document.getElementById("map-filter-ft8"); + const wsprFilter = document.getElementById("map-filter-wspr"); if (aprsFilter) { aprsFilter.addEventListener("change", () => { mapFilter.aprs = aprsFilter.checked; @@ -1056,6 +1057,12 @@ function initAprsMap() { applyMapFilter(); }); } + if (wsprFilter) { + wsprFilter.addEventListener("change", () => { + mapFilter.wspr = wsprFilter.checked; + applyMapFilter(); + }); + } } function sizeAprsMapToViewport() { @@ -1141,27 +1148,43 @@ function applyMapFilter() { if (!aprsMap) return; mapMarkers.forEach((marker) => { const type = marker.__trxType; - const visible = (type === "aprs" && mapFilter.aprs) || (type === "ft8" && mapFilter.ft8); + const visible = + (type === "aprs" && mapFilter.aprs) || + (type === "ft8" && mapFilter.ft8) || + (type === "wspr" && mapFilter.wspr); const onMap = aprsMap.hasLayer(marker); if (visible && !onMap) marker.addTo(aprsMap); if (!visible && onMap) marker.removeFrom(aprsMap); }); } -window.ft8MapAddLocator = function(message, grid) { +function escapeMapHtml(input) { + return String(input) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("\"", """); +} + +window.ft8MapAddLocator = function(message, grids, type = "ft8") { if (!aprsMap) initAprsMap(); if (!aprsMap) return; - const bounds = maidenheadToBounds(grid); - if (!bounds) return; - const popupContent = `${grid}
${message}`; - const marker = L.rectangle(bounds, { - color: "#ffb020", - weight: 1, - fillColor: "#ffb020", - fillOpacity: 0.25, - }).addTo(aprsMap).bindPopup(popupContent); - marker.__trxType = "ft8"; - mapMarkers.add(marker); + if (!Array.isArray(grids) || grids.length === 0) return; + const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))]; + const locatorsLines = unique.map((g) => escapeMapHtml(g)).join("
"); + for (const grid of unique) { + const bounds = maidenheadToBounds(grid); + if (!bounds) continue; + const popupContent = `${escapeMapHtml(grid)}
${locatorsLines}`; + const marker = L.rectangle(bounds, { + color: "#ffb020", + weight: 1, + fillColor: "#ffb020", + fillOpacity: 0.25, + }).addTo(aprsMap).bindPopup(popupContent); + marker.__trxType = type === "wspr" ? "wspr" : "ft8"; + mapMarkers.add(marker); + } applyMapFilter(); }; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index e8c8c84..f67705c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -165,6 +165,7 @@
+
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js index 6a56fbd..7c110b2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js @@ -70,7 +70,9 @@ function renderFt8Message(message) { return out; } -function extractFirstGrid(message) { +function extractAllGrids(message) { + const out = []; + const seen = new Set(); let i = 0; while (i < message.length) { if (isAlphaNum(message[i])) { @@ -78,13 +80,16 @@ function extractFirstGrid(message) { while (j < message.length && isAlphaNum(message[j])) j++; const token = message.slice(i, j); const grid = token.toUpperCase(); - if (/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(grid)) return grid; + if (/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(grid) && !seen.has(grid)) { + seen.add(grid); + out.push(grid); + } i = j; } else { i += 1; } } - return null; + return out; } function escapeHtml(input) { @@ -149,9 +154,9 @@ document.getElementById("ft8-clear-btn").addEventListener("click", async () => { // --- Server-side FT8 decode handler --- window.onServerFt8 = function(msg) { ft8Status.textContent = "Receiving"; - const grid = extractFirstGrid(msg.message || ""); - if (grid && window.ft8MapAddLocator) { - window.ft8MapAddLocator(msg.message, grid); + const grids = extractAllGrids((msg.message || "").toString()); + if (grids.length > 0 && window.ft8MapAddLocator) { + window.ft8MapAddLocator(msg.message || "", grids); } addFt8Message({ ts_ms: msg.ts_ms, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js index 4bfc32c..ef8f569 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js @@ -54,6 +54,20 @@ function escapeWsprHtml(input) { .replaceAll("\"", """); } +function extractAllGrids(message) { + const out = []; + const seen = new Set(); + const parts = message.toUpperCase().split(/[^A-Z0-9]+/); + for (const token of parts) { + if (!token) continue; + if (/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(token) && !seen.has(token)) { + seen.add(token); + out.push(token); + } + } + return out; +} + function applyWsprFilterToRow(row) { if (!wsprFilterText) { row.style.display = ""; @@ -86,11 +100,16 @@ document.getElementById("wspr-clear-btn").addEventListener("click", async () => window.onServerWspr = function(msg) { wsprStatus.textContent = "Receiving"; + const raw = (msg.message || "").toString(); + const grids = extractAllGrids(raw); + if (grids.length > 0 && window.ft8MapAddLocator) { + window.ft8MapAddLocator(raw, grids, "wspr"); + } addWsprMessage({ ts_ms: msg.ts_ms, snr_db: msg.snr_db, dt_s: msg.dt_s, freq_hz: msg.freq_hz, - message: msg.message, + message: raw, }); }; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 8bd32c1..98888f8 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -20,6 +20,9 @@ --audio-level-border: #2d3748; --audio-level-fill-start: #00d17f; --audio-level-fill-end: #f0ad4e; + --filter-bg: #1b2431; + --filter-fg: #e5e7eb; + --filter-border: #334155; } [data-theme="light"] { @@ -44,6 +47,9 @@ --audio-level-border: #b8c5da; --audio-level-fill-start: #0f9d61; --audio-level-fill-end: #b57600; + --filter-bg: #eef3fb; + --filter-fg: #1f2937; + --filter-border: #b8c5da; } body { font-family: sans-serif; margin: 0; min-height: 100vh; box-sizing: border-box; display: flex; align-items: flex-start; justify-content: center; padding-top: 2em; background: var(--bg); color: var(--text); } @@ -338,7 +344,16 @@ small { color: var(--text-muted); } .aprs-pos:hover { text-decoration: underline; } .aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; } .ft8-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; } -.ft8-filter { flex: 1; min-width: 10rem; } +.ft8-filter { + flex: 1; + min-width: 10rem; + background: var(--filter-bg); + color: var(--filter-fg); + border: 1px solid var(--filter-border); + border-radius: 6px; + padding: 0.45rem 0.55rem; +} +.ft8-filter::placeholder { color: color-mix(in srgb, var(--filter-fg) 55%, transparent); } .ft8-header { display: flex; gap: 0.6rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); border-bottom: 1px solid var(--border); padding: 0 0 0.35rem 0; margin-bottom: 0.35rem; } #ft8-messages { max-height: 360px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; padding: 0.35rem 0.5rem; } .ft8-row { display: flex; gap: 0.6rem; line-height: 1.4; border-bottom: 1px solid var(--border); padding: 0.25rem 0; } @@ -350,6 +365,16 @@ small { color: var(--text-muted); } .ft8-msg { flex: 1; } .ft8-locator { color: var(--accent-green); background: rgba(0, 209, 127, 0.12); border: 1px solid rgba(0, 209, 127, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-weight: 600; } .map-controls { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.6rem; color: var(--text-muted); font-size: 0.82rem; } +.map-controls label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.25rem 0.45rem; + border-radius: 999px; + border: 1px solid var(--filter-border); + background: var(--filter-bg); + color: var(--filter-fg); +} .map-controls input[type="checkbox"] { margin-right: 0.3rem; } .cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }