From 80de405a035f82d626926f4274742df64fb625aa Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 3 Mar 2026 21:53:59 +0100 Subject: [PATCH] [fix](trx-frontend): refine locator map controls and tooltips Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/app.js | 148 +++++++++++------- .../trx-frontend-http/assets/web/index.html | 1 + .../assets/web/plugins/ft8.js | 20 +-- .../assets/web/plugins/wspr.js | 2 +- .../trx-frontend-http/assets/web/style.css | 39 ++--- 5 files changed, 121 insertions(+), 89 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 f84038a..52c3841 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 @@ -3170,7 +3170,7 @@ let aprsRadioPath = null; const stationMarkers = new Map(); const locatorMarkers = new Map(); const mapMarkers = new Set(); -const mapFilter = { ais: true, vdes: true, aprs: true, ft8: true, wspr: true }; +const mapFilter = { ais: true, vdes: true, aprs: true, bookmark: true, ft8: true, wspr: true }; const APRS_TRACK_MAX_POINTS = 64; const AIS_TRACK_MAX_POINTS = 64; const aisMarkers = new Map(); @@ -3404,13 +3404,7 @@ function initAprsMap() { if (!bounds) continue; entry.marker = L.rectangle(bounds, locatorStyleForCount(entry.bookmarks?.length || 1, "bookmark")) .addTo(aprsMap) - .bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || [])) - .bindTooltip(buildBookmarkLocatorTooltipHtml(entry.grid, entry.bookmarks || []), { - className: "bookmark-locator-tip-shell", - direction: "top", - sticky: true, - opacity: 1, - }); + .bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || [])); entry.marker.__trxType = "bookmark"; mapMarkers.add(entry.marker); } @@ -3419,6 +3413,7 @@ function initAprsMap() { const aisFilter = document.getElementById("map-filter-ais"); const vdesFilter = document.getElementById("map-filter-vdes"); const aprsFilter = document.getElementById("map-filter-aprs"); + const bookmarkFilter = document.getElementById("map-filter-bookmark"); const ft8Filter = document.getElementById("map-filter-ft8"); const wsprFilter = document.getElementById("map-filter-wspr"); if (aisFilter) { @@ -3445,6 +3440,12 @@ function initAprsMap() { applyMapFilter(); }); } + if (bookmarkFilter) { + bookmarkFilter.addEventListener("change", () => { + mapFilter.bookmark = bookmarkFilter.checked; + applyMapFilter(); + }); + } if (ft8Filter) { ft8Filter.addEventListener("change", () => { mapFilter.ft8 = ft8Filter.checked; @@ -3990,7 +3991,7 @@ function applyMapFilter() { mapMarkers.forEach((marker) => { const type = marker.__trxType; const visible = - type === "bookmark" || + (type === "bookmark" && mapFilter.bookmark) || (type === "ais" && mapFilter.ais) || (type === "vdes" && mapFilter.vdes) || (type === "aprs" && mapFilter.aprs) || @@ -4024,6 +4025,57 @@ function locatorStyleForCount(count, type) { }; } +function formatDecodeLocatorTime(tsMs) { + if (!Number.isFinite(tsMs)) return "--:--:--"; + return new Date(tsMs).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +function buildDecodeLocatorTooltipHtml(grid, entry, type) { + const details = entry?.stationDetails instanceof Map + ? Array.from(entry.stationDetails.values()) + : []; + details.sort((a, b) => Number(b?.ts_ms || 0) - Number(a?.ts_ms || 0)); + const title = type === "wspr" ? "WSPR" : "FT8"; + const rows = details + .map((detail) => { + const station = escapeMapHtml(String(detail?.station || "Unknown")); + const freq = Number.isFinite(detail?.freq_hz) + ? `${Number(detail.freq_hz).toFixed(0)} Hz` + : "--"; + const meta = [ + Number.isFinite(detail?.snr_db) ? `${Number(detail.snr_db).toFixed(1)} dB` : null, + Number.isFinite(detail?.dt_s) ? `dt ${Number(detail.dt_s).toFixed(2)}` : null, + escapeMapHtml(freq), + ].filter(Boolean).join(" · "); + const message = detail?.message + ? `
${escapeMapHtml(String(detail.message))}
` + : ""; + return `
` + + `
` + + `${station}` + + `${escapeMapHtml(formatDecodeLocatorTime(Number(detail?.ts_ms)))}` + + `
` + + (meta ? `
${meta}
` : "") + + message + + `
`; + }) + .join(""); + const count = Math.max( + 1, + details.length, + entry?.stations instanceof Set ? entry.stations.size : 0, + ); + return `
` + + `
${escapeMapHtml(grid)}
` + + `
${title} · ${count} station${count === 1 ? "" : "s"}
` + + rows + + `
`; +} + function buildBookmarkLocatorPopupHtml(grid, bookmarks) { const list = Array.isArray(bookmarks) ? bookmarks : []; const rows = list @@ -4039,36 +4091,6 @@ function buildBookmarkLocatorPopupHtml(grid, bookmarks) { return `${escapeMapHtml(grid)}
Bookmarks: ${list.length || 1}` + (rows ? `
${rows}` : ""); } -function buildBookmarkLocatorTooltipHtml(grid, bookmarks) { - const list = Array.isArray(bookmarks) ? [...bookmarks] : []; - list.sort((a, b) => Number(a?.freq_hz || 0) - Number(b?.freq_hz || 0)); - const rows = list - .map((bm) => { - const name = escapeMapHtml(String(bm?.name || "Bookmark")); - const freq = typeof bmFmtFreq === "function" - ? escapeMapHtml(bmFmtFreq(bm?.freq_hz)) - : escapeMapHtml(String(bm?.freq_hz || "--")); - const meta = [ - bm?.mode ? escapeMapHtml(String(bm.mode)) : null, - bm?.category ? escapeMapHtml(String(bm.category)) : null, - ].filter(Boolean).join(" · "); - return `
` + - `
` + - `${name}` + - `${freq}` + - `
` + - (meta ? `
${meta}
` : "") + - (bm?.comment ? `
${escapeMapHtml(String(bm.comment))}
` : "") + - `
`; - }) - .join(""); - return `
` + - `
${escapeMapHtml(grid)}
` + - `
${list.length} bookmark${list.length === 1 ? "" : "s"}
` + - rows + - `
`; -} - window.syncBookmarkMapLocators = function(bookmarks) { const list = Array.isArray(bookmarks) ? bookmarks : []; const grouped = new Map(); @@ -4108,7 +4130,6 @@ window.syncBookmarkMapLocators = function(bookmarks) { existing.marker.setBounds(next.bounds); existing.marker.setStyle(locatorStyleForCount(next.bookmarks.length, "bookmark")); existing.marker.setPopupContent(popupHtml); - existing.marker.setTooltipContent(buildBookmarkLocatorTooltipHtml(next.grid, next.bookmarks)); } continue; } @@ -4123,13 +4144,7 @@ window.syncBookmarkMapLocators = function(bookmarks) { if (aprsMap) { entry.marker = L.rectangle(next.bounds, locatorStyleForCount(next.bookmarks.length, "bookmark")) .addTo(aprsMap) - .bindPopup(popupHtml) - .bindTooltip(buildBookmarkLocatorTooltipHtml(next.grid, next.bookmarks), { - className: "bookmark-locator-tip-shell", - direction: "top", - sticky: true, - opacity: 1, - }); + .bindPopup(popupHtml); entry.marker.__trxType = "bookmark"; mapMarkers.add(entry.marker); } @@ -4138,37 +4153,56 @@ window.syncBookmarkMapLocators = function(bookmarks) { applyMapFilter(); }; -window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null) { +window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null, details = null) { if (!aprsMap) initAprsMap(); if (!aprsMap) return; if (!Array.isArray(grids) || grids.length === 0) return; const markerType = type === "wspr" ? "wspr" : "ft8"; const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))]; - const locatorsLines = unique.map((g) => escapeMapHtml(g)).join("
"); + const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : ""; + const detailEntry = { + station: stationId || null, + ts_ms: Number.isFinite(details?.ts_ms) ? Number(details.ts_ms) : null, + snr_db: Number.isFinite(details?.snr_db) ? Number(details.snr_db) : null, + dt_s: Number.isFinite(details?.dt_s) ? Number(details.dt_s) : null, + freq_hz: Number.isFinite(details?.freq_hz) ? Number(details.freq_hz) : null, + message: String(details?.message || message || "").trim() || null, + }; + const detailKey = stationId || `${detailEntry.message || "decode"}:${detailEntry.ts_ms || Date.now()}`; for (const grid of unique) { const bounds = maidenheadToBounds(grid); if (!bounds) continue; const key = `${markerType}:${grid}`; - const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : ""; const existing = locatorMarkers.get(key); if (existing) { if (stationId) existing.stations.add(stationId); - const count = existing.stations.size || 1; + if (!(existing.stationDetails instanceof Map)) existing.stationDetails = new Map(); + existing.stationDetails.set(detailKey, { ...detailEntry }); + const count = Math.max(existing.stationDetails.size, existing.stations.size || 0, 1); + const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType); existing.marker.setStyle(locatorStyleForCount(count, markerType)); - existing.marker.setPopupContent( - `${escapeMapHtml(grid)}
Stations: ${count}
${locatorsLines}` - ); + existing.marker.setPopupContent(tooltipHtml); + existing.marker.setTooltipContent(tooltipHtml); continue; } const stations = new Set(); if (stationId) stations.add(stationId); - const count = stations.size || 1; + const stationDetails = new Map(); + stationDetails.set(detailKey, { ...detailEntry }); + const count = Math.max(stationDetails.size, stations.size || 0, 1); + const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, { stations, stationDetails }, markerType); const marker = L.rectangle(bounds, locatorStyleForCount(count, markerType)) .addTo(aprsMap) - .bindPopup(`${escapeMapHtml(grid)}
Stations: ${count}
${locatorsLines}`); + .bindPopup(tooltipHtml) + .bindTooltip(tooltipHtml, { + className: "decode-locator-tip-shell", + direction: "top", + sticky: true, + opacity: 1, + }); marker.__trxType = markerType; - locatorMarkers.set(key, { marker, stations }); + locatorMarkers.set(key, { marker, stations, stationDetails }); 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 be4b44b..3f13083 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 @@ -573,6 +573,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 a0db0f8..25fe5d1 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 @@ -30,12 +30,10 @@ function renderFt8Row(msg) { row.className = "ft8-row"; const rawMessage = (msg.message || "").toString(); row.dataset.message = rawMessage.toUpperCase(); - row.dataset.offsetHz = Number.isFinite(msg.freq_hz) ? String(msg.freq_hz) : ""; + row.dataset.storedFreqHz = Number.isFinite(msg.freq_hz) ? String(msg.freq_hz) : ""; const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--"; const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--"; - const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null; - const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null; - const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--"; + const freq = Number.isFinite(msg.freq_hz) ? msg.freq_hz.toFixed(0) : "--"; const renderedMessage = renderFt8Message(rawMessage); row.innerHTML = `${fmtTime(msg.ts_ms)}${snr}${dt}${freq}${renderedMessage}`; applyFt8FilterToRow(row); @@ -53,9 +51,8 @@ function addFt8Message(msg) { } function ft8BarRfText(msg) { - const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null; - if (!Number.isFinite(msg.freq_hz) || !Number.isFinite(baseHz)) return null; - return `${(baseHz + msg.freq_hz).toFixed(0)} Hz`; + if (!Number.isFinite(msg.freq_hz)) return null; + return `${msg.freq_hz.toFixed(0)} Hz`; } function updateFt8Bar() { @@ -175,10 +172,9 @@ function applyFt8FilterToAll() { function updateFt8RowRf(row) { const freqEl = row.querySelector(".ft8-freq"); if (!freqEl) return; - const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null; - const offset = row.dataset.offsetHz ? Number(row.dataset.offsetHz) : NaN; - if (Number.isFinite(baseHz) && Number.isFinite(offset)) { - freqEl.textContent = (baseHz + offset).toFixed(0); + const storedFreqHz = row.dataset.storedFreqHz ? Number(row.dataset.storedFreqHz) : NaN; + if (Number.isFinite(storedFreqHz)) { + freqEl.textContent = storedFreqHz.toFixed(0); } else { freqEl.textContent = "--"; } @@ -224,7 +220,7 @@ window.onServerFt8 = function(msg) { const grids = extractAllGrids(raw); const station = extractLikelyCallsign(raw); if (grids.length > 0 && window.ft8MapAddLocator) { - window.ft8MapAddLocator(raw, grids, "ft8", station); + window.ft8MapAddLocator(raw, grids, "ft8", station, msg); } addFt8Message({ receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null, 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 2bbea28..8c85e6a 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 @@ -125,7 +125,7 @@ window.onServerWspr = function(msg) { const grids = extractAllGrids(raw); const station = extractLikelyCallsign(raw); if (grids.length > 0 && window.ft8MapAddLocator) { - window.ft8MapAddLocator(raw, grids, "wspr", station); + window.ft8MapAddLocator(raw, grids, "wspr", station, msg); } addWsprMessage({ receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null, 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 6821d55..9662e49 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 @@ -1456,67 +1456,68 @@ small { color: var(--text-muted); } .aprs-popup-table td { padding: 0.06rem 0.3rem 0.06rem 0; vertical-align: top; font-size: 0.88em; } .aprs-popup-label { color: var(--text-muted); white-space: nowrap; padding-right: 0.5rem !important; } .aprs-popup-info { font-size: 0.85em; color: var(--text); border-top: 1px solid var(--border-light); padding-top: 0.25rem; margin-top: 0.1rem; word-break: break-word; } -.bookmark-locator-tip-shell { +.decode-locator-tip-shell { background: transparent; border: none; box-shadow: none; } -.bookmark-locator-tip-shell .leaflet-tooltip-content { +.decode-locator-tip-shell .leaflet-tooltip-content { margin: 0; } -.bookmark-locator-tip-shell.leaflet-tooltip-top::before { +.decode-locator-tip-shell.leaflet-tooltip-top::before { border-top-color: color-mix(in srgb, var(--card-bg) 94%, transparent); } -.bookmark-locator-tip { - min-width: 15rem; - max-width: 24rem; +.decode-locator-tip { + min-width: 16rem; + max-width: 26rem; + max-height: min(22rem, 60vh); + overflow: auto; padding: 0.55rem 0.65rem; - border: 1px solid color-mix(in srgb, var(--accent-green) 26%, var(--border-light)); + border: 1px solid color-mix(in srgb, var(--accent-yellow) 26%, var(--border-light)); border-radius: 0.65rem; background: color-mix(in srgb, var(--card-bg) 94%, transparent); color: var(--text); box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28); } -.bookmark-locator-tip-title { - color: var(--accent-green); +.decode-locator-tip-title { + color: var(--accent-yellow); font-weight: 700; font-size: 0.9rem; letter-spacing: 0.03em; } -.bookmark-locator-tip-subtitle { +.decode-locator-tip-subtitle { margin-top: 0.1rem; margin-bottom: 0.45rem; font-size: 0.75rem; color: var(--text-muted); } -.bookmark-locator-tip-row + .bookmark-locator-tip-row { +.decode-locator-tip-row + .decode-locator-tip-row { margin-top: 0.45rem; padding-top: 0.4rem; border-top: 1px solid color-mix(in srgb, var(--border-light) 70%, transparent); } -.bookmark-locator-tip-head { +.decode-locator-tip-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; } -.bookmark-locator-tip-name { +.decode-locator-tip-name { font-weight: 600; color: var(--text-heading); } -.bookmark-locator-tip-freq { +.decode-locator-tip-time { flex: 0 0 auto; - font-size: 0.78rem; - color: var(--accent-yellow); - font-variant-numeric: tabular-nums; + font-size: 0.75rem; + color: var(--text-muted); white-space: nowrap; } -.bookmark-locator-tip-meta { +.decode-locator-tip-meta { margin-top: 0.12rem; font-size: 0.74rem; color: var(--text-muted); } -.bookmark-locator-tip-note { +.decode-locator-tip-note { margin-top: 0.2rem; font-size: 0.76rem; line-height: 1.3;