[feat](trx-rs): map VDES positions and restore burst gating
Publish decoded VDES positions into the map and revert the VDES burst detector to its original gating thresholds. Co-authored-by: Stan Grams <sjg@haxx.space> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3139,10 +3139,11 @@ let aprsRadioPath = null;
|
||||
const stationMarkers = new Map();
|
||||
const locatorMarkers = new Map();
|
||||
const mapMarkers = new Set();
|
||||
const mapFilter = { ais: true, aprs: true, ft8: true, wspr: true };
|
||||
const mapFilter = { ais: true, vdes: true, aprs: true, ft8: true, wspr: true };
|
||||
const APRS_TRACK_MAX_POINTS = 64;
|
||||
const AIS_TRACK_MAX_POINTS = 64;
|
||||
const aisMarkers = new Map();
|
||||
const vdesMarkers = new Map();
|
||||
let selectedAisTrackMmsi = null;
|
||||
|
||||
function syncAprsReceiverMarker() {
|
||||
@@ -3205,6 +3206,17 @@ window.clearMapMarkersByType = function(type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "vdes") {
|
||||
vdesMarkers.forEach((entry) => {
|
||||
if (entry && entry.marker) {
|
||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||
mapMarkers.delete(entry.marker);
|
||||
}
|
||||
});
|
||||
vdesMarkers.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "ft8" || type === "wspr") {
|
||||
const prefix = `${type}:`;
|
||||
for (const [key, entry] of locatorMarkers.entries()) {
|
||||
@@ -3311,6 +3323,19 @@ function initAprsMap() {
|
||||
{ className: "aprs-radio-path", weight: 2, interactive: false }
|
||||
).addTo(aprsMap);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (marker._vdesKey) {
|
||||
const entry = vdesMarkers.get(String(marker._vdesKey));
|
||||
if (!entry || !entry.msg) return;
|
||||
e.popup.setContent(buildVdesPopupHtml(entry.msg));
|
||||
if (serverLat != null && serverLon != null) {
|
||||
aprsRadioPath = L.polyline(
|
||||
[[serverLat, serverLon], [ll.lat, ll.lng]],
|
||||
{ className: "aprs-radio-path", weight: 2, interactive: false }
|
||||
).addTo(aprsMap);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3334,6 +3359,7 @@ function initAprsMap() {
|
||||
applyMapFilter();
|
||||
|
||||
const aisFilter = document.getElementById("map-filter-ais");
|
||||
const vdesFilter = document.getElementById("map-filter-vdes");
|
||||
const aprsFilter = document.getElementById("map-filter-aprs");
|
||||
const ft8Filter = document.getElementById("map-filter-ft8");
|
||||
const wsprFilter = document.getElementById("map-filter-wspr");
|
||||
@@ -3349,6 +3375,12 @@ function initAprsMap() {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (vdesFilter) {
|
||||
vdesFilter.addEventListener("change", () => {
|
||||
mapFilter.vdes = vdesFilter.checked;
|
||||
applyMapFilter();
|
||||
});
|
||||
}
|
||||
if (aprsFilter) {
|
||||
aprsFilter.addEventListener("change", () => {
|
||||
mapFilter.aprs = aprsFilter.checked;
|
||||
@@ -3528,6 +3560,43 @@ function buildAisPopupHtml(msg) {
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
function buildVdesPopupHtml(msg) {
|
||||
const age = formatTimeAgo(msg?.ts_ms);
|
||||
const distKm = (serverLat != null && serverLon != null && msg?.lat != null && msg?.lon != null)
|
||||
? haversineKm(serverLat, serverLon, msg.lat, msg.lon)
|
||||
: null;
|
||||
const distStr = distKm != null
|
||||
? (distKm < 1 ? `${Math.round(distKm * 1000)} m` : `${distKm.toFixed(1)} km`)
|
||||
: null;
|
||||
const meta = [
|
||||
age,
|
||||
distStr,
|
||||
msg?.message_label ? escapeMapHtml(msg.message_label) : null,
|
||||
Number.isFinite(msg?.link_id) ? `LID ${Number(msg.link_id)}` : null,
|
||||
].filter(Boolean).join(" · ");
|
||||
let rows = "";
|
||||
if (distStr) rows += `<tr><td class="aprs-popup-label">Range</td><td>${distStr} from TRX</td></tr>`;
|
||||
rows += `<tr><td class="aprs-popup-label">Type</td><td>${escapeMapHtml(String(msg?.message_type ?? "--"))}</td></tr>`;
|
||||
if (Number.isFinite(msg?.source_id)) rows += `<tr><td class="aprs-popup-label">Source</td><td>${escapeMapHtml(String(msg.source_id))}</td></tr>`;
|
||||
if (Number.isFinite(msg?.destination_id)) rows += `<tr><td class="aprs-popup-label">Dest</td><td>${escapeMapHtml(String(msg.destination_id))}</td></tr>`;
|
||||
if (msg?.lat != null && msg?.lon != null) rows += `<tr><td class="aprs-popup-label">Pos</td><td>${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}</td></tr>`;
|
||||
if (Number.isFinite(msg?.sync_score)) rows += `<tr><td class="aprs-popup-label">Sync</td><td>${(Number(msg.sync_score) * 100).toFixed(0)}%</td></tr>`;
|
||||
if (msg?.fec_state) rows += `<tr><td class="aprs-popup-label">FEC</td><td>${escapeMapHtml(String(msg.fec_state))}</td></tr>`;
|
||||
const info = [
|
||||
msg?.vessel_name,
|
||||
msg?.callsign,
|
||||
msg?.destination,
|
||||
msg?.payload_preview,
|
||||
].filter(Boolean).map(escapeMapHtml).join(" · ");
|
||||
const title = escapeMapHtml(msg?.vessel_name || msg?.callsign || "VDES Position");
|
||||
return `<div class="aprs-popup">` +
|
||||
`<div class="aprs-popup-call">${title}</div>` +
|
||||
(meta ? `<div class="aprs-popup-meta">${meta}</div>` : "") +
|
||||
(rows ? `<table class="aprs-popup-table">${rows}</table>` : "") +
|
||||
(info ? `<div class="aprs-popup-info">${info}</div>` : "") +
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
function aprsPositionsEqual(a, b) {
|
||||
if (!a || !b) return false;
|
||||
return Math.abs(a[0] - b[0]) < 0.000001 && Math.abs(a[1] - b[1]) < 0.000001;
|
||||
@@ -3538,6 +3607,15 @@ function aisPositionsEqual(a, b) {
|
||||
return Math.abs(a[0] - b[0]) < 0.000001 && Math.abs(a[1] - b[1]) < 0.000001;
|
||||
}
|
||||
|
||||
function vdesMarkerKey(msg) {
|
||||
if (Number.isFinite(msg?.source_id)) return `src:${Number(msg.source_id)}`;
|
||||
if (Number.isFinite(msg?.mmsi) && Number(msg.mmsi) > 0) return `mmsi:${Number(msg.mmsi)}`;
|
||||
if (msg?.lat != null && msg?.lon != null) {
|
||||
return `pos:${Number(msg.lat).toFixed(4)}:${Number(msg.lon).toFixed(4)}:${Number(msg?.message_type ?? 0)}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureAprsTrack(call, entry) {
|
||||
if (!aprsMap || !entry || !Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) return;
|
||||
if (entry.track) {
|
||||
@@ -3719,6 +3797,40 @@ window.aisMapAddVessel = function(msg) {
|
||||
applyMapFilter();
|
||||
};
|
||||
|
||||
window.vdesMapAddPoint = function(msg) {
|
||||
if (msg == null || msg.lat == null || msg.lon == null) return;
|
||||
const key = vdesMarkerKey(msg);
|
||||
if (!key) return;
|
||||
if (!aprsMap) initAprsMap();
|
||||
const popupHtml = buildVdesPopupHtml(msg);
|
||||
const existing = vdesMarkers.get(key);
|
||||
if (existing) {
|
||||
existing.msg = msg;
|
||||
if (existing.marker) {
|
||||
existing.marker.setLatLng([msg.lat, msg.lon]);
|
||||
existing.marker.setPopupContent(popupHtml);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const entry = {
|
||||
marker: null,
|
||||
msg,
|
||||
};
|
||||
vdesMarkers.set(key, entry);
|
||||
if (!aprsMap) return;
|
||||
const marker = L.circleMarker([msg.lat, msg.lon], {
|
||||
radius: 5,
|
||||
color: "#5c394f",
|
||||
fillColor: "#c46392",
|
||||
fillOpacity: 0.82,
|
||||
}).addTo(aprsMap).bindPopup(popupHtml);
|
||||
marker.__trxType = "vdes";
|
||||
marker._vdesKey = key;
|
||||
entry.marker = marker;
|
||||
mapMarkers.add(marker);
|
||||
applyMapFilter();
|
||||
};
|
||||
|
||||
function maidenheadToBounds(grid) {
|
||||
if (!grid || grid.length < 4) return null;
|
||||
const g = grid.toUpperCase();
|
||||
@@ -3754,6 +3866,7 @@ function applyMapFilter() {
|
||||
const type = marker.__trxType;
|
||||
const visible =
|
||||
(type === "ais" && mapFilter.ais) ||
|
||||
(type === "vdes" && mapFilter.vdes) ||
|
||||
(type === "aprs" && mapFilter.aprs) ||
|
||||
(type === "ft8" && mapFilter.ft8) ||
|
||||
(type === "wspr" && mapFilter.wspr);
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
<div id="tab-map" class="tab-panel" style="display:none;">
|
||||
<div class="map-controls">
|
||||
<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-ft8" checked /> FT8</label>
|
||||
<label><input type="checkbox" id="map-filter-wspr" checked /> WSPR</label>
|
||||
|
||||
@@ -244,6 +244,8 @@ window.onServerVdes = function(msg) {
|
||||
message_type: msg.message_type,
|
||||
bit_len: msg.bit_len,
|
||||
raw_bytes: msg.raw_bytes,
|
||||
lat: msg.lat,
|
||||
lon: msg.lon,
|
||||
vessel_name: msg.vessel_name,
|
||||
callsign: msg.callsign,
|
||||
destination: msg.destination,
|
||||
@@ -263,6 +265,9 @@ window.onServerVdes = function(msg) {
|
||||
fec_state: msg.fec_state,
|
||||
ts_ms: msg.ts_ms,
|
||||
});
|
||||
if (msg.lat != null && msg.lon != null && window.vdesMapAddPoint) {
|
||||
window.vdesMapAddPoint(msg);
|
||||
}
|
||||
};
|
||||
|
||||
updateVdesSummary();
|
||||
|
||||
Reference in New Issue
Block a user