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; }