[fix](trx-frontend): refine locator map controls and tooltips
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -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
|
||||
? `<div class="decode-locator-tip-note">${escapeMapHtml(String(detail.message))}</div>`
|
||||
: "";
|
||||
return `<div class="decode-locator-tip-row">` +
|
||||
`<div class="decode-locator-tip-head">` +
|
||||
`<span class="decode-locator-tip-name">${station}</span>` +
|
||||
`<span class="decode-locator-tip-time">${escapeMapHtml(formatDecodeLocatorTime(Number(detail?.ts_ms)))}</span>` +
|
||||
`</div>` +
|
||||
(meta ? `<div class="decode-locator-tip-meta">${meta}</div>` : "") +
|
||||
message +
|
||||
`</div>`;
|
||||
})
|
||||
.join("");
|
||||
const count = Math.max(
|
||||
1,
|
||||
details.length,
|
||||
entry?.stations instanceof Set ? entry.stations.size : 0,
|
||||
);
|
||||
return `<div class="decode-locator-tip">` +
|
||||
`<div class="decode-locator-tip-title">${escapeMapHtml(grid)}</div>` +
|
||||
`<div class="decode-locator-tip-subtitle">${title} · ${count} station${count === 1 ? "" : "s"}</div>` +
|
||||
rows +
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
function buildBookmarkLocatorPopupHtml(grid, bookmarks) {
|
||||
const list = Array.isArray(bookmarks) ? bookmarks : [];
|
||||
const rows = list
|
||||
@@ -4039,36 +4091,6 @@ function buildBookmarkLocatorPopupHtml(grid, bookmarks) {
|
||||
return `<b>${escapeMapHtml(grid)}</b><br>Bookmarks: ${list.length || 1}` + (rows ? `<br>${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 `<div class="bookmark-locator-tip-row">` +
|
||||
`<div class="bookmark-locator-tip-head">` +
|
||||
`<span class="bookmark-locator-tip-name">${name}</span>` +
|
||||
`<span class="bookmark-locator-tip-freq">${freq}</span>` +
|
||||
`</div>` +
|
||||
(meta ? `<div class="bookmark-locator-tip-meta">${meta}</div>` : "") +
|
||||
(bm?.comment ? `<div class="bookmark-locator-tip-note">${escapeMapHtml(String(bm.comment))}</div>` : "") +
|
||||
`</div>`;
|
||||
})
|
||||
.join("");
|
||||
return `<div class="bookmark-locator-tip">` +
|
||||
`<div class="bookmark-locator-tip-title">${escapeMapHtml(grid)}</div>` +
|
||||
`<div class="bookmark-locator-tip-subtitle">${list.length} bookmark${list.length === 1 ? "" : "s"}</div>` +
|
||||
rows +
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
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("<br>");
|
||||
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(
|
||||
`<b>${escapeMapHtml(grid)}</b><br>Stations: ${count}<br>${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(`<b>${escapeMapHtml(grid)}</b><br>Stations: ${count}<br>${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();
|
||||
|
||||
@@ -573,6 +573,7 @@
|
||||
<label><input type="checkbox" id="map-filter-ais" checked /> AIS</label>
|
||||
<label><input type="checkbox" id="map-filter-vdes" checked /> VDES</label>
|
||||
<label><input type="checkbox" id="map-filter-aprs" checked /> APRS</label>
|
||||
<label><input type="checkbox" id="map-filter-bookmark" checked /> Bookmark Locators</label>
|
||||
<label><input type="checkbox" id="map-filter-ft8" checked /> FT8</label>
|
||||
<label><input type="checkbox" id="map-filter-wspr" checked /> WSPR</label>
|
||||
</div>
|
||||
|
||||
@@ -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 = `<span class="ft8-time">${fmtTime(msg.ts_ms)}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderedMessage}</span>`;
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user