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;