[fix](trx): normalize FT8 history freq and add map locator links
This commit is contained in:
@@ -4417,6 +4417,60 @@ window.navigateToAprsMap = function(lat, lon) {
|
||||
}
|
||||
};
|
||||
|
||||
window.navigateToMapLocator = function(grid, preferredType = null) {
|
||||
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
||||
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
||||
|
||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => t.classList.remove("active"));
|
||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
||||
document.querySelectorAll(".tab-panel").forEach((p) => (p.style.display = "none"));
|
||||
const mapPanel = document.getElementById("tab-map");
|
||||
if (mapPanel) mapPanel.style.display = "";
|
||||
|
||||
initAprsMap();
|
||||
sizeAprsMapToViewport();
|
||||
if (!aprsMap) return false;
|
||||
|
||||
const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft8" ? "ft8" : null);
|
||||
const keys = pref
|
||||
? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]
|
||||
: [`ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
|
||||
let entry = null;
|
||||
for (const key of keys) {
|
||||
entry = locatorMarkers.get(key);
|
||||
if (entry?.marker) break;
|
||||
}
|
||||
if (!entry?.marker) return false;
|
||||
|
||||
if (pref && Object.prototype.hasOwnProperty.call(mapFilter, pref) && !mapFilter[pref]) {
|
||||
mapFilter[pref] = true;
|
||||
rebuildMapLocatorFilters();
|
||||
applyMapFilter();
|
||||
}
|
||||
|
||||
const marker = entry.marker;
|
||||
if (!aprsMap.hasLayer(marker)) {
|
||||
marker.addTo(aprsMap);
|
||||
sendLocatorOverlayToBack(marker);
|
||||
}
|
||||
const center = locatorMarkerCenter(marker);
|
||||
const focusMarker = () => {
|
||||
if (!aprsMap || !marker) return;
|
||||
aprsMap.invalidateSize();
|
||||
if (center) {
|
||||
const targetZoom = Math.max(aprsMap.getZoom() || 0, 7);
|
||||
aprsMap.setView([center.lat, center.lon], targetZoom);
|
||||
setMapRadioPathTo(center.lat, center.lon, "locator-radio-path");
|
||||
}
|
||||
setSelectedLocatorMarker(marker);
|
||||
if (typeof marker.openPopup === "function") marker.openPopup();
|
||||
};
|
||||
focusMarker();
|
||||
setTimeout(focusMarker, 60);
|
||||
return true;
|
||||
};
|
||||
|
||||
function haversineKm(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
|
||||
@@ -43,6 +43,7 @@ function renderFt8Row(msg) {
|
||||
row.className = "ft8-row";
|
||||
const rawMessage = (msg.message || "").toString();
|
||||
row.dataset.message = rawMessage.toUpperCase();
|
||||
row.dataset.decoder = "ft8";
|
||||
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) : "--";
|
||||
@@ -131,7 +132,7 @@ function renderFt8Message(message) {
|
||||
const token = message.slice(i, j);
|
||||
const grid = token.toUpperCase();
|
||||
if (isMaidenheadGridToken(grid)) {
|
||||
out += `<span class="ft8-locator">${grid}</span>`;
|
||||
out += `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>`;
|
||||
} else {
|
||||
out += escapeHtml(token);
|
||||
}
|
||||
@@ -200,6 +201,17 @@ function isAlphaNum(ch) {
|
||||
return /[A-Za-z0-9]/.test(ch);
|
||||
}
|
||||
|
||||
function activateFt8HistoryLocator(targetEl) {
|
||||
const locatorEl = targetEl?.closest?.(".ft8-locator[data-locator-grid]");
|
||||
if (!locatorEl) return false;
|
||||
const grid = String(locatorEl.dataset.locatorGrid || "").toUpperCase();
|
||||
if (!grid) return false;
|
||||
if (typeof window.navigateToMapLocator === "function") {
|
||||
window.navigateToMapLocator(grid, "ft8");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFt8FilterToRow(row) {
|
||||
if (!ft8FilterText) {
|
||||
row.style.display = "";
|
||||
@@ -248,6 +260,20 @@ if (ft8FilterInput) {
|
||||
});
|
||||
}
|
||||
|
||||
if (ft8MessagesEl) {
|
||||
ft8MessagesEl.addEventListener("click", (event) => {
|
||||
if (!activateFt8HistoryLocator(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
ft8MessagesEl.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
if (!activateFt8HistoryLocator(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
if (ft8PauseBtn) {
|
||||
ft8PauseBtn.addEventListener("click", () => {
|
||||
ft8Paused = !ft8Paused;
|
||||
@@ -279,10 +305,7 @@ window.onServerFt8 = function(msg) {
|
||||
const raw = (msg.message || "").toString();
|
||||
const grids = extractAllGrids(raw);
|
||||
const station = extractLikelyCallsign(raw);
|
||||
const baseHz = Number.isFinite(window.ft8BaseHz) ? Number(window.ft8BaseHz) : null;
|
||||
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz)
|
||||
? (baseHz + Number(msg.freq_hz))
|
||||
: (Number.isFinite(msg.freq_hz) ? Number(msg.freq_hz) : null);
|
||||
const rfHz = normalizeFt8DisplayFreqHz(msg.freq_hz);
|
||||
if (grids.length > 0 && window.ft8MapAddLocator) {
|
||||
window.ft8MapAddLocator(raw, grids, "ft8", station, {
|
||||
...msg,
|
||||
@@ -294,7 +317,7 @@ window.onServerFt8 = function(msg) {
|
||||
ts_ms: msg.ts_ms,
|
||||
snr_db: msg.snr_db,
|
||||
dt_s: msg.dt_s,
|
||||
freq_hz: msg.freq_hz,
|
||||
freq_hz: Number.isFinite(rfHz) ? rfHz : msg.freq_hz,
|
||||
message: msg.message,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ setInterval(updateWsprPeriodTimer, 500);
|
||||
function renderWsprRow(msg) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ft8-row";
|
||||
row.dataset.decoder = "wspr";
|
||||
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;
|
||||
@@ -38,7 +39,7 @@ function renderWsprRow(msg) {
|
||||
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
||||
const message = (msg.message || "").toString();
|
||||
row.dataset.message = message.toUpperCase();
|
||||
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(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">${escapeWsprHtml(message)}</span>`;
|
||||
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(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">${renderWsprMessage(message)}</span>`;
|
||||
applyWsprFilterToRow(row);
|
||||
return row;
|
||||
}
|
||||
@@ -80,6 +81,30 @@ function escapeWsprHtml(input) {
|
||||
.replaceAll("\"", """);
|
||||
}
|
||||
|
||||
function renderWsprMessage(message) {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while (i < message.length) {
|
||||
const ch = message[i];
|
||||
if (isAlphaNum(ch)) {
|
||||
let j = i + 1;
|
||||
while (j < message.length && isAlphaNum(message[j])) j++;
|
||||
const token = message.slice(i, j);
|
||||
const grid = token.toUpperCase();
|
||||
if (isMaidenheadGridToken(grid)) {
|
||||
out += `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>`;
|
||||
} else {
|
||||
out += escapeWsprHtml(token);
|
||||
}
|
||||
i = j;
|
||||
} else {
|
||||
out += escapeWsprHtml(ch);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractAllGrids(message) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
@@ -116,6 +141,21 @@ function isMaidenheadGridToken(token) {
|
||||
return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized);
|
||||
}
|
||||
|
||||
function isAlphaNum(ch) {
|
||||
return /[A-Za-z0-9]/.test(ch);
|
||||
}
|
||||
|
||||
function activateWsprHistoryLocator(targetEl) {
|
||||
const locatorEl = targetEl?.closest?.(".ft8-locator[data-locator-grid]");
|
||||
if (!locatorEl) return false;
|
||||
const grid = String(locatorEl.dataset.locatorGrid || "").toUpperCase();
|
||||
if (!grid) return false;
|
||||
if (typeof window.navigateToMapLocator === "function") {
|
||||
window.navigateToMapLocator(grid, "wspr");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyWsprFilterToRow(row) {
|
||||
if (!wsprFilterText) {
|
||||
row.style.display = "";
|
||||
@@ -145,6 +185,20 @@ if (wsprFilterInput) {
|
||||
});
|
||||
}
|
||||
|
||||
if (wsprMessagesEl) {
|
||||
wsprMessagesEl.addEventListener("click", (event) => {
|
||||
if (!activateWsprHistoryLocator(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
wsprMessagesEl.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
if (!activateWsprHistoryLocator(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
if (wsprPauseBtn) {
|
||||
wsprPauseBtn.addEventListener("click", () => {
|
||||
wsprPaused = !wsprPaused;
|
||||
|
||||
@@ -1634,6 +1634,9 @@ small { color: var(--text-muted); }
|
||||
.ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; }
|
||||
.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; }
|
||||
.ft8-locator[data-locator-grid] { cursor: pointer; user-select: none; }
|
||||
.ft8-locator[data-locator-grid]:hover { filter: brightness(1.12); }
|
||||
.ft8-locator[data-locator-grid]:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent-green) 66%, transparent); outline-offset: 1px; }
|
||||
.map-locator-filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1341,11 +1341,17 @@ pub async fn run_ft8_decoder(
|
||||
Ok(dur) => dur.as_millis() as i64,
|
||||
Err(_) => 0,
|
||||
};
|
||||
let base_freq_hz = state_rx.borrow().status.freq.hz as f64;
|
||||
let abs_freq_hz = base_freq_hz + res.freq_hz as f64;
|
||||
let msg = Ft8Message {
|
||||
ts_ms,
|
||||
snr_db: res.snr_db,
|
||||
dt_s: res.dt_s,
|
||||
freq_hz: res.freq_hz,
|
||||
freq_hz: if abs_freq_hz.is_finite() && abs_freq_hz > 0.0 {
|
||||
abs_freq_hz as f32
|
||||
} else {
|
||||
res.freq_hz
|
||||
},
|
||||
message: res.text,
|
||||
};
|
||||
histories.record_ft8_message(msg.clone());
|
||||
|
||||
@@ -179,7 +179,12 @@ fn now_unix_seconds() -> u32 {
|
||||
}
|
||||
|
||||
fn offset_to_abs(base_freq_hz: u64, offset_hz: f32) -> u64 {
|
||||
let freq = base_freq_hz as f64 + offset_hz as f64;
|
||||
// Accept both legacy decoder offsets (~kHz audio tones) and already-absolute RF Hz.
|
||||
let raw = offset_hz as f64;
|
||||
if raw.is_finite() && raw >= 100_000.0 {
|
||||
return raw.round() as u64;
|
||||
}
|
||||
let freq = base_freq_hz as f64 + raw;
|
||||
if freq.is_finite() && freq > 0.0 {
|
||||
freq.round() as u64
|
||||
} else {
|
||||
@@ -454,4 +459,10 @@ mod tests {
|
||||
let grid = maidenhead_from_lat_lon(52.2297, 21.0122);
|
||||
assert_eq!(grid.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_to_abs_accepts_offset_and_absolute() {
|
||||
assert_eq!(offset_to_abs(14_074_000, 1_237.0), 14_075_237);
|
||||
assert_eq!(offset_to_abs(14_074_000, 14_075_237.0), 14_075_237);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user