[feat](trx-frontend-http): add ft8 locators and map filters
Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -704,6 +704,8 @@ connect();
|
||||
let aprsMap = null;
|
||||
let aprsMapReceiverMarker = null;
|
||||
const stationMarkers = new Map();
|
||||
const mapMarkers = new Set();
|
||||
const mapFilter = { aprs: true, ft8: true };
|
||||
|
||||
function initAprsMap() {
|
||||
if (aprsMap) return;
|
||||
@@ -726,6 +728,21 @@ function initAprsMap() {
|
||||
radius: 8, color: "#3388ff", fillColor: "#3388ff", fillOpacity: 0.8
|
||||
}).addTo(aprsMap).bindPopup(popupText);
|
||||
}
|
||||
|
||||
const aprsFilter = document.getElementById("map-filter-aprs");
|
||||
const ft8Filter = document.getElementById("map-filter-ft8");
|
||||
if (aprsFilter) {
|
||||
aprsFilter.addEventListener("change", () => {
|
||||
mapFilter.aprs = aprsFilter.checked;
|
||||
applyMapFilter();
|
||||
});
|
||||
}
|
||||
if (ft8Filter) {
|
||||
ft8Filter.addEventListener("change", () => {
|
||||
mapFilter.ft8 = ft8Filter.checked;
|
||||
applyMapFilter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function aprsSymbolIcon(symbolTable, symbolCode) {
|
||||
@@ -752,8 +769,8 @@ window.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCod
|
||||
const popupContent = `<b>${call}</b><br>${info}`;
|
||||
const existing = stationMarkers.get(call);
|
||||
if (existing) {
|
||||
existing.setLatLng([lat, lon]);
|
||||
existing.setPopupContent(popupContent);
|
||||
existing.marker.setLatLng([lat, lon]);
|
||||
existing.marker.setPopupContent(popupContent);
|
||||
} else {
|
||||
const icon = aprsSymbolIcon(symbolTable, symbolCode);
|
||||
const marker = icon
|
||||
@@ -761,10 +778,55 @@ window.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCod
|
||||
: L.circleMarker([lat, lon], {
|
||||
radius: 6, color: "#00d17f", fillColor: "#00d17f", fillOpacity: 0.8
|
||||
}).addTo(aprsMap).bindPopup(popupContent);
|
||||
stationMarkers.set(call, marker);
|
||||
marker.__trxType = "aprs";
|
||||
stationMarkers.set(call, { marker, type: "aprs" });
|
||||
mapMarkers.add(marker);
|
||||
applyMapFilter();
|
||||
}
|
||||
};
|
||||
|
||||
function maidenheadToLatLon(grid) {
|
||||
if (!grid || grid.length < 4) return null;
|
||||
const g = grid.toUpperCase();
|
||||
const A = "A".charCodeAt(0);
|
||||
const lon = (g.charCodeAt(0) - A) * 20 - 180 + (parseInt(g[2], 10) * 2) + 1;
|
||||
const lat = (g.charCodeAt(1) - A) * 10 - 90 + (parseInt(g[3], 10) * 1) + 0.5;
|
||||
let lonAdj = lon;
|
||||
let latAdj = lat;
|
||||
if (g.length >= 6) {
|
||||
const subLon = (g.charCodeAt(4) - A) * (5 / 60);
|
||||
const subLat = (g.charCodeAt(5) - A) * (2.5 / 60);
|
||||
lonAdj += subLon - (5 / 120);
|
||||
latAdj += subLat - (2.5 / 120);
|
||||
}
|
||||
return { lat: latAdj, lon: lonAdj };
|
||||
}
|
||||
|
||||
function applyMapFilter() {
|
||||
if (!aprsMap) return;
|
||||
mapMarkers.forEach((marker) => {
|
||||
const type = marker.__trxType;
|
||||
const visible = (type === "aprs" && mapFilter.aprs) || (type === "ft8" && mapFilter.ft8);
|
||||
const onMap = aprsMap.hasLayer(marker);
|
||||
if (visible && !onMap) marker.addTo(aprsMap);
|
||||
if (!visible && onMap) marker.removeFrom(aprsMap);
|
||||
});
|
||||
}
|
||||
|
||||
window.ft8MapAddLocator = function(message, grid) {
|
||||
if (!aprsMap) initAprsMap();
|
||||
if (!aprsMap) return;
|
||||
const loc = maidenheadToLatLon(grid);
|
||||
if (!loc) return;
|
||||
const popupContent = `<b>${grid}</b><br>${message}`;
|
||||
const marker = L.circleMarker([loc.lat, loc.lon], {
|
||||
radius: 5, color: "#ffb020", fillColor: "#ffb020", fillOpacity: 0.85
|
||||
}).addTo(aprsMap).bindPopup(popupContent);
|
||||
marker.__trxType = "ft8";
|
||||
mapMarkers.add(marker);
|
||||
applyMapFilter();
|
||||
};
|
||||
|
||||
// --- Sub-tab navigation (Plugins tab) ---
|
||||
document.querySelectorAll(".sub-tab-bar").forEach((bar) => {
|
||||
bar.addEventListener("click", (e) => {
|
||||
|
||||
@@ -148,6 +148,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="subtab-map" class="sub-tab-panel" style="display:none;">
|
||||
<div class="map-controls">
|
||||
<label><input type="checkbox" id="map-filter-aprs" checked /> APRS</label>
|
||||
<label><input type="checkbox" id="map-filter-ft8" checked /> FT8</label>
|
||||
</div>
|
||||
<div id="aprs-map"></div>
|
||||
</div>
|
||||
<div id="subtab-aprs" class="sub-tab-panel" style="display:none;">
|
||||
|
||||
@@ -16,7 +16,8 @@ function renderFt8Row(msg) {
|
||||
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) : "--";
|
||||
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">${msg.message || ""}</span>`;
|
||||
const renderedMessage = renderFt8Message(msg.message || "");
|
||||
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>`;
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,20 @@ function addFt8Message(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderFt8Message(message) {
|
||||
const gridRegex = /(^|\\s)([A-R]{2}\\d{2}(?:[A-X]{2})?)(?=\\s|$)/gi;
|
||||
return message.replace(gridRegex, (match, lead, grid) => {
|
||||
const safeGrid = grid.toUpperCase();
|
||||
return `${lead}<span class="ft8-locator">[${safeGrid}]</span>`;
|
||||
});
|
||||
}
|
||||
|
||||
function extractFirstGrid(message) {
|
||||
const gridRegex = /(^|\\s)([A-R]{2}\\d{2}(?:[A-X]{2})?)(?=\\s|$)/i;
|
||||
const match = message.match(gridRegex);
|
||||
return match ? match[2].toUpperCase() : null;
|
||||
}
|
||||
|
||||
document.getElementById("ft8-decode-toggle-btn").addEventListener("click", async () => {
|
||||
try { await postPath("/toggle_ft8_decode"); } catch (e) { console.error("FT8 toggle failed", e); }
|
||||
});
|
||||
@@ -39,6 +54,10 @@ 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);
|
||||
}
|
||||
addFt8Message({
|
||||
ts_ms: msg.ts_ms,
|
||||
snr_db: msg.snr_db,
|
||||
|
||||
@@ -233,6 +233,9 @@ small { color: var(--text-muted); }
|
||||
.ft8-dt { color: var(--text-muted); min-width: 3.6rem; text-align: right; }
|
||||
.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; }
|
||||
.map-controls { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.6rem; color: var(--text-muted); font-size: 0.82rem; }
|
||||
.map-controls input[type="checkbox"] { margin-right: 0.3rem; }
|
||||
|
||||
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
|
||||
.cw-config { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
|
||||
|
||||
Reference in New Issue
Block a user