[fix](trx-frontend-http): show the map and statistics the whole picture #56
+1
-1
@@ -464,7 +464,7 @@ and each page answers that differently:
|
|||||||
|------|-------|
|
|------|-------|
|
||||||
| Radio | The selected rig: its spectrum, its audio, and the mini decode views over the waterfall. |
|
| Radio | The selected rig: its spectrum, its audio, and the mini decode views over the waterfall. |
|
||||||
| Digital Modes | The selected rig: every decoder panel, its counts and its status line. |
|
| Digital Modes | The selected rig: every decoder panel, its counts and its status line. |
|
||||||
| Map | The whole station — every rig's positions, with the map's own rig filter to narrow it. |
|
| Map | The whole station — every rig's positions, with the map's own rig filter to narrow it. HF APRS is a source of its own there, filtered apart from VHF APRS. |
|
||||||
| Statistics | The whole station, including the per-rig comparison. |
|
| Statistics | The whole station, including the per-rig comparison. |
|
||||||
|
|
||||||
Switching rigs repaints the radio and digital modes pages for the rig now
|
Switching rigs repaints the radio and digital modes pages for the rig now
|
||||||
|
|||||||
@@ -5889,6 +5889,7 @@ function navigateToTab(name, options = {}) {
|
|||||||
if (leavingSatellites) window.clearSatPredictionDom?.();
|
if (leavingSatellites) window.clearSatPredictionDom?.();
|
||||||
void loadPluginsForTab(name).then(() => {
|
void loadPluginsForTab(name).then(() => {
|
||||||
if (name === "satellites") window.refreshSatPredictions?.();
|
if (name === "satellites") window.refreshSatPredictions?.();
|
||||||
|
flushPendingDecodeStats();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
@@ -7619,11 +7620,35 @@ var IMAGE_DECODE_KINDS = /* @__PURE__ */ new Set([
|
|||||||
"sstv",
|
"sstv",
|
||||||
"sstv_progress"
|
"sstv_progress"
|
||||||
]);
|
]);
|
||||||
|
var pendingDecodeStats = [];
|
||||||
|
var PENDING_DECODE_STATS_MAX = 5e4;
|
||||||
|
function recordDecodeStat(kind, rig, tsMs) {
|
||||||
|
const stats = window.trx.modules.map;
|
||||||
|
if (stats) {
|
||||||
|
stats.statsRecordDecode(kind, rig, tsMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingDecodeStats.push({ kind, rig, tsMs });
|
||||||
|
if (pendingDecodeStats.length > PENDING_DECODE_STATS_MAX) {
|
||||||
|
pendingDecodeStats.splice(0, pendingDecodeStats.length - PENDING_DECODE_STATS_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function flushPendingDecodeStats() {
|
||||||
|
const stats = window.trx.modules.map;
|
||||||
|
if (!stats || pendingDecodeStats.length === 0) return;
|
||||||
|
for (const entry of pendingDecodeStats.splice(0)) {
|
||||||
|
stats.statsRecordDecode(entry.kind, entry.rig, entry.tsMs);
|
||||||
|
}
|
||||||
|
stats.scheduleStatsRender();
|
||||||
|
}
|
||||||
|
function scheduleStatsRenderIfLoaded() {
|
||||||
|
window.trx.modules.map?.scheduleStatsRender();
|
||||||
|
}
|
||||||
function dispatchDecodeMessage(msg, skipStats = false) {
|
function dispatchDecodeMessage(msg, skipStats = false) {
|
||||||
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
|
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
|
||||||
if (!skipStats && msg.type && !IMAGE_DECODE_KINDS.has(msg.type)) {
|
if (!skipStats && msg.type && !IMAGE_DECODE_KINDS.has(msg.type)) {
|
||||||
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
|
recordDecodeStat(msg.type, msg.rig_id || msg.remote || null);
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
scheduleStatsRenderIfLoaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var DECODE_HISTORY_WORKER_GROUP_LIMIT = 512;
|
var DECODE_HISTORY_WORKER_GROUP_LIMIT = 512;
|
||||||
@@ -7669,9 +7694,9 @@ function restoreDecodeHistoryGroup(kind, messages) {
|
|||||||
if (!Array.isArray(messages) || messages.length === 0) return;
|
if (!Array.isArray(messages) || messages.length === 0) return;
|
||||||
if (!IMAGE_DECODE_KINDS.has(kind)) {
|
if (!IMAGE_DECODE_KINDS.has(kind)) {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
window.trx.modules.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || void 0);
|
recordDecodeStat(kind, msg.rig_id || msg.remote || null, msg.ts_ms || void 0);
|
||||||
}
|
}
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
scheduleStatsRenderIfLoaded();
|
||||||
}
|
}
|
||||||
window.trxPluginRuntime.restore(kind, messages);
|
window.trxPluginRuntime.restore(kind, messages);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-8
@@ -168,19 +168,22 @@ function initializeFtxDecoder(config) {
|
|||||||
}
|
}
|
||||||
messagesElement.replaceChildren(fragment);
|
messagesElement.replaceChildren(fragment);
|
||||||
};
|
};
|
||||||
const normalize = (message) => {
|
const plotLocator = (message) => {
|
||||||
const raw = message.message ?? "";
|
const raw = message.message ?? "";
|
||||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||||
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||||
|
if (grids.length === 0) return;
|
||||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||||
const frequency = displayFrequency(message.freq_hz);
|
const frequency = displayFrequency(message.freq_hz);
|
||||||
if (grids.length > 0) {
|
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
...message,
|
||||||
...message,
|
freq_hz: frequency ?? message.freq_hz,
|
||||||
freq_hz: frequency ?? message.freq_hz,
|
locator_details: locatorDetails
|
||||||
locator_details: locatorDetails
|
});
|
||||||
});
|
};
|
||||||
}
|
const normalize = (message) => {
|
||||||
|
const frequency = displayFrequency(message.freq_hz);
|
||||||
|
plotLocator(message);
|
||||||
return {
|
return {
|
||||||
// The rig that heard it, kept so the mini view can tell a decode of the
|
// The rig that heard it, kept so the mini view can tell a decode of the
|
||||||
// rig on screen from one a background rig made on another band.
|
// rig on screen from one a background rig made on another band.
|
||||||
@@ -240,6 +243,10 @@ function initializeFtxDecoder(config) {
|
|||||||
rerender: () => {
|
rerender: () => {
|
||||||
bridge.updateFt8Bar?.();
|
bridge.updateFt8Bar?.();
|
||||||
render();
|
render();
|
||||||
|
},
|
||||||
|
// Oldest first, so the map builds the grids up in the order they were heard.
|
||||||
|
syncMap: () => {
|
||||||
|
for (const message of [...history].reverse()) plotLocator(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
initializeFtxDecoder
|
initializeFtxDecoder
|
||||||
} from "./chunk-PAKFJPA2.js";
|
} from "./chunk-PJ5Q7CWJ.js";
|
||||||
import "./chunk-S57W63QN.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
initializeFtxDecoder
|
initializeFtxDecoder
|
||||||
} from "./chunk-PAKFJPA2.js";
|
} from "./chunk-PJ5Q7CWJ.js";
|
||||||
import "./chunk-S57W63QN.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
initializeFt8FamilyBar,
|
initializeFt8FamilyBar,
|
||||||
initializeFtxDecoder,
|
initializeFtxDecoder,
|
||||||
installFtxCompatibilityHelpers
|
installFtxCompatibilityHelpers
|
||||||
} from "./chunk-PAKFJPA2.js";
|
} from "./chunk-PJ5Q7CWJ.js";
|
||||||
import "./chunk-S57W63QN.js";
|
import "./chunk-S57W63QN.js";
|
||||||
import "./chunk-KL66PICH.js";
|
import "./chunk-KL66PICH.js";
|
||||||
|
|
||||||
|
|||||||
@@ -149,17 +149,32 @@ function resetHfAprsHistoryView() {
|
|||||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||||
hfAprsPacketHistory = [];
|
hfAprsPacketHistory = [];
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
|
hfAprsWindow.clearMapMarkersByType?.("hf_aprs");
|
||||||
}
|
}
|
||||||
function pruneHfAprsHistoryView() {
|
function pruneHfAprsHistoryView() {
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
}
|
}
|
||||||
|
function plotHfAprsPacket(pkt) {
|
||||||
|
if (pkt.lat == null || pkt.lon == null || !hfAprsWindow.aprsMapAddStation) return;
|
||||||
|
hfAprsWindow.aprsMapAddStation(
|
||||||
|
pkt.srcCall ?? "",
|
||||||
|
pkt.lat,
|
||||||
|
pkt.lon,
|
||||||
|
pkt.info ?? "",
|
||||||
|
pkt.symbolTable,
|
||||||
|
pkt.symbolCode,
|
||||||
|
pkt,
|
||||||
|
"hf_aprs"
|
||||||
|
);
|
||||||
|
}
|
||||||
function addHfAprsPacket(pkt) {
|
function addHfAprsPacket(pkt) {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||||
pkt._tsMs = tsMs;
|
pkt._tsMs = tsMs;
|
||||||
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
hfAprsPacketHistory.unshift(pkt);
|
hfAprsPacketHistory.unshift(pkt);
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
|
plotHfAprsPacket(pkt);
|
||||||
scheduleHfAprsHistoryRender();
|
scheduleHfAprsHistoryRender();
|
||||||
}
|
}
|
||||||
function normalizeServerHfAprsPacket(pkt) {
|
function normalizeServerHfAprsPacket(pkt) {
|
||||||
@@ -174,6 +189,7 @@ function onServerHfAprsBatch(packets) {
|
|||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
|
plotHfAprsPacket(next);
|
||||||
normalized.push(next);
|
normalized.push(next);
|
||||||
}
|
}
|
||||||
normalized.reverse();
|
normalized.reverse();
|
||||||
@@ -248,5 +264,9 @@ window.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerHfAprsBatch,
|
restore: onServerHfAprsBatch,
|
||||||
reset: resetHfAprsHistoryView,
|
reset: resetHfAprsHistoryView,
|
||||||
prune: pruneHfAprsHistoryView,
|
prune: pruneHfAprsHistoryView,
|
||||||
rerender: renderHfAprsHistory
|
rerender: renderHfAprsHistory,
|
||||||
|
// Oldest first, so station tracks are rebuilt in the order they happened.
|
||||||
|
syncMap: () => {
|
||||||
|
for (const entry of [...hfAprsPacketHistory].reverse()) plotHfAprsPacket(entry);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ var mapWindow = window;
|
|||||||
const decodeContactPaths = /* @__PURE__ */ new Map();
|
const decodeContactPaths = /* @__PURE__ */ new Map();
|
||||||
let selectedMapQsoKey = null;
|
let selectedMapQsoKey = null;
|
||||||
const mapMarkers = /* @__PURE__ */ new Set();
|
const mapMarkers = /* @__PURE__ */ new Set();
|
||||||
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, hf_aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
||||||
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||||
const MAP_FILTER_ALL_KEY = "__all";
|
const MAP_FILTER_ALL_KEY = "__all";
|
||||||
const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
|
const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
|
||||||
@@ -128,6 +128,12 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
function aprsEntrySource(entry) {
|
||||||
|
return entry?.type === "hf_aprs" ? "hf_aprs" : "aprs";
|
||||||
|
}
|
||||||
|
function aprsStationKey(call, source) {
|
||||||
|
return source === "aprs" ? call : `${source}:${call}`;
|
||||||
|
}
|
||||||
function refreshAprsTrack(call, entry) {
|
function refreshAprsTrack(call, entry) {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
|
if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
|
||||||
@@ -149,7 +155,7 @@ var mapWindow = window;
|
|||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
interactive: false
|
interactive: false
|
||||||
});
|
});
|
||||||
track.__trxType = "aprs";
|
track.__trxType = aprsEntrySource(entry);
|
||||||
track._aprsCall = call;
|
track._aprsCall = call;
|
||||||
entry.track = track;
|
entry.track = track;
|
||||||
}
|
}
|
||||||
@@ -381,6 +387,7 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
function mapSourceLabel(type) {
|
function mapSourceLabel(type) {
|
||||||
if (type === "bookmark") return "Bookmarks";
|
if (type === "bookmark") return "Bookmarks";
|
||||||
|
if (type === "hf_aprs") return "HF APRS";
|
||||||
return String(type || "").toUpperCase();
|
return String(type || "").toUpperCase();
|
||||||
}
|
}
|
||||||
function locatorFilterColor(type) {
|
function locatorFilterColor(type) {
|
||||||
@@ -396,6 +403,7 @@ var mapWindow = window;
|
|||||||
if (type === "vdes") return "#a78bfa";
|
if (type === "vdes") return "#a78bfa";
|
||||||
if (type === "sat") return "#f59e0b";
|
if (type === "sat") return "#f59e0b";
|
||||||
if (type === "aprs") return "#00d17f";
|
if (type === "aprs") return "#00d17f";
|
||||||
|
if (type === "hf_aprs") return "#fb7185";
|
||||||
return locatorFilterColor(type);
|
return locatorFilterColor(type);
|
||||||
}
|
}
|
||||||
function bandForHz(hz) {
|
function bandForHz(hz) {
|
||||||
@@ -931,10 +939,8 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const entry of stationMarkers.values()) {
|
for (const entry of stationMarkers.values()) {
|
||||||
if (entry?.type === "aprs" && entry?.visibleInHistoryWindow) {
|
if (!entry?.visibleInHistoryWindow) continue;
|
||||||
availableSources.add("aprs");
|
availableSources.add(aprsEntrySource(entry));
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const bandMap = /* @__PURE__ */ new Map();
|
const bandMap = /* @__PURE__ */ new Map();
|
||||||
for (const entry of locatorMarkers.values()) {
|
for (const entry of locatorMarkers.values()) {
|
||||||
@@ -966,7 +972,7 @@ var mapWindow = window;
|
|||||||
for (const key of Array.from(mapLocatorFilter.bands)) {
|
for (const key of Array.from(mapLocatorFilter.bands)) {
|
||||||
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
||||||
}
|
}
|
||||||
const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"].filter((key) => availableSources.has(key)).map((key) => ({
|
const sourceItems = ["ais", "vdes", "aprs", "hf_aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"].filter((key) => availableSources.has(key)).map((key) => ({
|
||||||
key,
|
key,
|
||||||
label: mapSourceLabel(key),
|
label: mapSourceLabel(key),
|
||||||
color: mapSourceColor(key),
|
color: mapSourceColor(key),
|
||||||
@@ -1028,9 +1034,10 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
return parts.join(" ").toLowerCase();
|
return parts.join(" ").toLowerCase();
|
||||||
}
|
}
|
||||||
if (type === "aprs") {
|
if (type === "aprs" || type === "hf_aprs") {
|
||||||
const call = marker?._aprsCall ? String(marker._aprsCall) : "";
|
const key = marker?._aprsCall ? String(marker._aprsCall) : "";
|
||||||
const entry = stationMarkers.get(call);
|
const entry = stationMarkers.get(key);
|
||||||
|
const call = entry?.call ?? key;
|
||||||
const info = entry?.info ? String(entry.info) : "";
|
const info = entry?.info ? String(entry.info) : "";
|
||||||
const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
|
const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
|
||||||
return `${call} ${info} ${pktRaw}`.toLowerCase();
|
return `${call} ${info} ${pktRaw}`.toLowerCase();
|
||||||
@@ -1200,9 +1207,10 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
mapWindow.clearMapMarkersByType = function(type) {
|
mapWindow.clearMapMarkersByType = function(type) {
|
||||||
if (type === "aprs") {
|
if (type === "aprs" || type === "hf_aprs") {
|
||||||
selectedAprsTrackCall = null;
|
selectedAprsTrackCall = null;
|
||||||
stationMarkers.forEach((entry) => {
|
stationMarkers.forEach((entry, key) => {
|
||||||
|
if (aprsEntrySource(entry) !== type) return;
|
||||||
if (entry && entry.marker) {
|
if (entry && entry.marker) {
|
||||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.marker);
|
mapMarkers.delete(entry.marker);
|
||||||
@@ -1211,8 +1219,8 @@ var mapWindow = window;
|
|||||||
if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.track);
|
mapMarkers.delete(entry.track);
|
||||||
}
|
}
|
||||||
|
stationMarkers.delete(key);
|
||||||
});
|
});
|
||||||
stationMarkers.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === "ais") {
|
if (type === "ais") {
|
||||||
@@ -1448,13 +1456,15 @@ var mapWindow = window;
|
|||||||
if (!ll) return;
|
if (!ll) return;
|
||||||
const entry = stationMarkers.get(marker._aprsCall);
|
const entry = stationMarkers.get(marker._aprsCall);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
const source = aprsEntrySource(entry);
|
||||||
|
const call = entry.call ?? String(marker._aprsCall);
|
||||||
|
e.popup.setContent(buildAprsPopupHtml(call, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
||||||
refreshAprsTrack(String(marker._aprsCall), entry);
|
refreshAprsTrack(String(marker._aprsCall), entry);
|
||||||
if (entry.track && aprsMap && mapFilter.aprs && !aprsMap.hasLayer(entry.track)) {
|
if (entry.track && aprsMap && mapFilter[source] && !aprsMap.hasLayer(entry.track)) {
|
||||||
entry.track.addTo(aprsMap);
|
entry.track.addTo(aprsMap);
|
||||||
}
|
}
|
||||||
selectedAprsTrackCall = String(marker._aprsCall);
|
selectedAprsTrackCall = String(marker._aprsCall);
|
||||||
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path", marker.__trxRigIds);
|
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor(source), "aprs-radio-path", marker.__trxRigIds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (marker._aisMmsi) {
|
if (marker._aisMmsi) {
|
||||||
@@ -1848,28 +1858,33 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function _aprsAddMarkerToMap(call, entry) {
|
function _aprsAddMarkerToMap(key, entry) {
|
||||||
if (!aprsMap || entry.lat == null || entry.lon == null) return;
|
if (!aprsMap || entry.lat == null || entry.lon == null) return;
|
||||||
refreshAprsTrack(call, entry);
|
refreshAprsTrack(key, entry);
|
||||||
|
const source = aprsEntrySource(entry);
|
||||||
|
const call = entry.call ?? key;
|
||||||
const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
|
const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
|
||||||
const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
|
const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
|
||||||
|
const color = mapSourceColor(source);
|
||||||
const marker = icon ? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent) : L.circleMarker([entry.lat, entry.lon], {
|
const marker = icon ? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent) : L.circleMarker([entry.lat, entry.lon], {
|
||||||
radius: 6,
|
radius: 6,
|
||||||
color: "#00d17f",
|
color,
|
||||||
fillColor: "#00d17f",
|
fillColor: color,
|
||||||
fillOpacity: 0.8
|
fillOpacity: 0.8
|
||||||
}).addTo(aprsMap).bindPopup(popupContent);
|
}).addTo(aprsMap).bindPopup(popupContent);
|
||||||
marker.__trxType = "aprs";
|
marker.__trxType = source;
|
||||||
marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
|
marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
|
||||||
marker._aprsCall = call;
|
marker._aprsCall = key;
|
||||||
entry.marker = marker;
|
entry.marker = marker;
|
||||||
mapMarkers.add(marker);
|
mapMarkers.add(marker);
|
||||||
}
|
}
|
||||||
mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt) {
|
mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt, source) {
|
||||||
const nextPoint = [lat, lon];
|
const nextPoint = [lat, lon];
|
||||||
const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
|
const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
|
||||||
const msgRigId = pkt?.rig_id || T.lastActiveRigId;
|
const msgRigId = pkt?.rig_id || T.lastActiveRigId;
|
||||||
const existing = stationMarkers.get(call);
|
const stationSource = source === "hf_aprs" ? "hf_aprs" : "aprs";
|
||||||
|
const key = aprsStationKey(call, stationSource);
|
||||||
|
const existing = stationMarkers.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.pkt = pkt;
|
existing.pkt = pkt;
|
||||||
existing.lat = lat;
|
existing.lat = lat;
|
||||||
@@ -1888,7 +1903,8 @@ var mapWindow = window;
|
|||||||
} else if (prevPoint) {
|
} else if (prevPoint) {
|
||||||
prevPoint.tsMs = tsMs;
|
prevPoint.tsMs = tsMs;
|
||||||
}
|
}
|
||||||
pruneAprsEntry(call, existing, mapHistoryCutoffMs());
|
existing.call = call;
|
||||||
|
pruneAprsEntry(key, existing, mapHistoryCutoffMs());
|
||||||
if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
|
if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
|
||||||
existing.marker.setLatLng([lat, lon]);
|
existing.marker.setLatLng([lat, lon]);
|
||||||
existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
|
existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
|
||||||
@@ -1899,7 +1915,8 @@ var mapWindow = window;
|
|||||||
track: null,
|
track: null,
|
||||||
trackHistory: [{ lat, lon, tsMs }],
|
trackHistory: [{ lat, lon, tsMs }],
|
||||||
trackPoints: [nextPoint],
|
trackPoints: [nextPoint],
|
||||||
type: "aprs",
|
type: stationSource,
|
||||||
|
call,
|
||||||
pkt,
|
pkt,
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
@@ -1908,9 +1925,9 @@ var mapWindow = window;
|
|||||||
symbolCode,
|
symbolCode,
|
||||||
rigIds: new Set(msgRigId ? [msgRigId] : [])
|
rigIds: new Set(msgRigId ? [msgRigId] : [])
|
||||||
};
|
};
|
||||||
stationMarkers.set(call, entry);
|
stationMarkers.set(key, entry);
|
||||||
pruneAprsEntry(call, entry, mapHistoryCutoffMs());
|
pruneAprsEntry(key, entry, mapHistoryCutoffMs());
|
||||||
if (entry.visibleInHistoryWindow) ensureAprsMarker(call, entry);
|
if (entry.visibleInHistoryWindow) ensureAprsMarker(key, entry);
|
||||||
if (aprsMap) scheduleDecodeMapMaintenance();
|
if (aprsMap) scheduleDecodeMapMaintenance();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ function normalizeServerWsprMessage(msg) {
|
|||||||
rfHz,
|
rfHz,
|
||||||
history: {
|
history: {
|
||||||
rig_id: msg.rig_id ?? null,
|
rig_id: msg.rig_id ?? null,
|
||||||
|
_rfHz: rfHz,
|
||||||
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
||||||
ts_ms: msg.ts_ms,
|
ts_ms: msg.ts_ms,
|
||||||
snr_db: msg.snr_db,
|
snr_db: msg.snr_db,
|
||||||
@@ -110,12 +111,7 @@ function onServerWsprBatch(messages) {
|
|||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
plotWsprLocator(msg);
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
|
||||||
...msg,
|
|
||||||
...next.rfHz === null ? {} : { freq_hz: next.rfHz }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
||||||
normalized.push(next.history);
|
normalized.push(next.history);
|
||||||
}
|
}
|
||||||
@@ -255,15 +251,19 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
function plotWsprLocator(msg) {
|
||||||
|
const next = normalizeServerWsprMessage(msg);
|
||||||
|
if (next.grids.length === 0 || !wsprWindow.mapAddLocator) return;
|
||||||
|
const rfHz = finiteNumber(msg._rfHz) ?? next.rfHz;
|
||||||
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
|
...msg,
|
||||||
|
...rfHz === null ? {} : { freq_hz: rfHz }
|
||||||
|
});
|
||||||
|
}
|
||||||
function onServerWspr(msg) {
|
function onServerWspr(msg) {
|
||||||
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
plotWsprLocator(msg);
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
|
||||||
...msg,
|
|
||||||
...next.rfHz === null ? {} : { freq_hz: next.rfHz }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addWsprMessage(next.history);
|
addWsprMessage(next.history);
|
||||||
}
|
}
|
||||||
wsprWindow.trxPluginRuntime.registerDecoder({
|
wsprWindow.trxPluginRuntime.registerDecoder({
|
||||||
@@ -273,5 +273,9 @@ wsprWindow.trxPluginRuntime.registerDecoder({
|
|||||||
restore: onServerWsprBatch,
|
restore: onServerWsprBatch,
|
||||||
prune: pruneWsprHistoryView,
|
prune: pruneWsprHistoryView,
|
||||||
reset: resetWsprHistoryView,
|
reset: resetWsprHistoryView,
|
||||||
rerender: renderWsprHistory
|
rerender: renderWsprHistory,
|
||||||
|
// Oldest first, so the map builds the grids up in the order they were heard.
|
||||||
|
syncMap: () => {
|
||||||
|
for (const message of [...wsprMessageHistory].reverse()) plotWsprLocator(message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4912,6 +4912,8 @@ function navigateToTab(name: TabName, options: { updateHistory?: boolean; replac
|
|||||||
// Passes go stale while the page is closed, so each visit reloads them.
|
// Passes go stale while the page is closed, so each visit reloads them.
|
||||||
// The first visit is what imports the module, which renders on its own.
|
// The first visit is what imports the module, which renders on its own.
|
||||||
if (name === "satellites") window.refreshSatPredictions?.();
|
if (name === "satellites") window.refreshSatPredictions?.();
|
||||||
|
// The map module owns the decode log; this is the moment it can exist.
|
||||||
|
flushPendingDecodeStats();
|
||||||
}).catch((error: unknown) => { console.error(error); });
|
}).catch((error: unknown) => { console.error(error); });
|
||||||
if (name === "map") {
|
if (name === "map") {
|
||||||
_initMapWhenReady();
|
_initMapWhenReady();
|
||||||
@@ -6597,11 +6599,49 @@ const IMAGE_DECODE_KINDS = new Set([
|
|||||||
"lrpt_image", "lrpt_progress", "wefax", "wefax_progress", "sstv", "sstv_progress",
|
"lrpt_image", "lrpt_progress", "wefax", "wefax_progress", "sstv", "sstv_progress",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// The decode log the Statistics page counts lives in the map module, which is
|
||||||
|
// lazy: it arrives when the Map or Statistics tab is first opened, long after
|
||||||
|
// the history replayed and the live decodes started coming. Recording into a
|
||||||
|
// module that is not there yet dropped every one of them, and nothing replayed
|
||||||
|
// them afterwards -- so the page opened empty and only filled in from decodes
|
||||||
|
// heard after it, which is why it took a reload (landing on the tab, so the
|
||||||
|
// module loads at startup) to show the whole picture. Hold them here until
|
||||||
|
// the module arrives, then hand them over.
|
||||||
|
interface PendingDecodeStat { kind: string; rig: string | null; tsMs: number | undefined }
|
||||||
|
const pendingDecodeStats: PendingDecodeStat[] = [];
|
||||||
|
// The module's own log keeps 50k entries; there is no point holding more here.
|
||||||
|
const PENDING_DECODE_STATS_MAX = 50_000;
|
||||||
|
|
||||||
|
function recordDecodeStat(kind: string, rig: string | null, tsMs?: number) {
|
||||||
|
const stats = window.trx.modules.map;
|
||||||
|
if (stats) {
|
||||||
|
stats.statsRecordDecode(kind, rig, tsMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingDecodeStats.push({ kind, rig, tsMs });
|
||||||
|
if (pendingDecodeStats.length > PENDING_DECODE_STATS_MAX) {
|
||||||
|
pendingDecodeStats.splice(0, pendingDecodeStats.length - PENDING_DECODE_STATS_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushPendingDecodeStats() {
|
||||||
|
const stats = window.trx.modules.map;
|
||||||
|
if (!stats || pendingDecodeStats.length === 0) return;
|
||||||
|
for (const entry of pendingDecodeStats.splice(0)) {
|
||||||
|
stats.statsRecordDecode(entry.kind, entry.rig, entry.tsMs);
|
||||||
|
}
|
||||||
|
stats.scheduleStatsRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleStatsRenderIfLoaded() {
|
||||||
|
window.trx.modules.map?.scheduleStatsRender();
|
||||||
|
}
|
||||||
|
|
||||||
function dispatchDecodeMessage(msg: DecodeMessage, skipStats = false) {
|
function dispatchDecodeMessage(msg: DecodeMessage, skipStats = false) {
|
||||||
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
|
if (msg.type) window.trxPluginRuntime.dispatch(msg.type, msg);
|
||||||
if (!skipStats && msg.type && !IMAGE_DECODE_KINDS.has(msg.type)) {
|
if (!skipStats && msg.type && !IMAGE_DECODE_KINDS.has(msg.type)) {
|
||||||
window.trx.modules.map?.statsRecordDecode(msg.type, msg.rig_id || msg.remote || null);
|
recordDecodeStat(msg.type, msg.rig_id || msg.remote || null);
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
scheduleStatsRenderIfLoaded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6651,9 +6691,9 @@ function restoreDecodeHistoryGroup(kind: string, messages: DecodeMessage[]) {
|
|||||||
// Record statistics for restored history messages.
|
// Record statistics for restored history messages.
|
||||||
if (!IMAGE_DECODE_KINDS.has(kind)) {
|
if (!IMAGE_DECODE_KINDS.has(kind)) {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
window.trx.modules.map?.statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined);
|
recordDecodeStat(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined);
|
||||||
}
|
}
|
||||||
window.trx.modules.map?.scheduleStatsRender();
|
scheduleStatsRenderIfLoaded();
|
||||||
}
|
}
|
||||||
window.trxPluginRuntime.restore(kind, messages);
|
window.trxPluginRuntime.restore(kind, messages);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function mapEl(id: string): MapElement {
|
|||||||
return element as MapElement;
|
return element as MapElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DecoderSource = "ais" | "vdes" | "aprs" | "bookmark" | "ft8" | "ft4" | "ft2" | "wspr" | "sat";
|
type DecoderSource = "ais" | "vdes" | "aprs" | "hf_aprs" | "bookmark" | "ft8" | "ft4" | "ft2" | "wspr" | "sat";
|
||||||
interface TrackPoint { lat: number; lon: number; tsMs: number }
|
interface TrackPoint { lat: number; lon: number; tsMs: number }
|
||||||
interface DecodeDetail {
|
interface DecodeDetail {
|
||||||
station?: string | null; source?: string | null; target?: string | null;
|
station?: string | null; source?: string | null; target?: string | null;
|
||||||
@@ -83,6 +83,9 @@ interface MapEntry {
|
|||||||
bookmarks?: Array<Record<string, unknown>>;
|
bookmarks?: Array<Record<string, unknown>>;
|
||||||
bounds?: Leaflet.LatLngBoundsExpression;
|
bounds?: Leaflet.LatLngBoundsExpression;
|
||||||
symbolTable?: string; symbolCode?: string; bandLabel?: string | null;
|
symbolTable?: string; symbolCode?: string; bandLabel?: string | null;
|
||||||
|
/** Callsign as heard. The map key qualifies it with the source, so that a
|
||||||
|
* station worked on both 144 MHz and 30 m keeps one marker per band. */
|
||||||
|
call?: string;
|
||||||
overlay?: TrxLayer | null; line?: TrxLayer | null; labelMarker?: TrxLayer | null;
|
overlay?: TrxLayer | null; line?: TrxLayer | null; labelMarker?: TrxLayer | null;
|
||||||
pathKey?: string; sourceGrid?: string; targetGrid?: string;
|
pathKey?: string; sourceGrid?: string; targetGrid?: string;
|
||||||
from?: LatLon; to?: LatLon;
|
from?: LatLon; to?: LatLon;
|
||||||
@@ -171,7 +174,7 @@ interface MapWindow {
|
|||||||
clearMapMarkersByType?(type: DecoderSource): void;
|
clearMapMarkersByType?(type: DecoderSource): void;
|
||||||
navigateToAprsMap?(lat: number, lon: number): void;
|
navigateToAprsMap?(lat: number, lon: number): void;
|
||||||
navigateToMapLocator?(grid: string, preferredType?: string | null): void;
|
navigateToMapLocator?(grid: string, preferredType?: string | null): void;
|
||||||
aprsMapAddStation?(call: string, lat: number, lon: number, info: string, symbolTable: string, symbolCode: string, packet: MapMessage): void;
|
aprsMapAddStation?(call: string, lat: number, lon: number, info: string, symbolTable: string, symbolCode: string, packet: MapMessage, source?: "aprs" | "hf_aprs"): void;
|
||||||
aisMapAddVessel?(message: MapMessage): void;
|
aisMapAddVessel?(message: MapMessage): void;
|
||||||
vdesMapAddPoint?(message: MapMessage): void;
|
vdesMapAddPoint?(message: MapMessage): void;
|
||||||
syncBookmarkMapLocators?(bookmarks: Array<Record<string, unknown>>): void;
|
syncBookmarkMapLocators?(bookmarks: Array<Record<string, unknown>>): void;
|
||||||
@@ -228,7 +231,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
const decodeContactPaths = new Map<string, MapEntry>();
|
const decodeContactPaths = new Map<string, MapEntry>();
|
||||||
let selectedMapQsoKey: string | null = null;
|
let selectedMapQsoKey: string | null = null;
|
||||||
const mapMarkers = new Set<TrxLayer>();
|
const mapMarkers = new Set<TrxLayer>();
|
||||||
const DEFAULT_MAP_SOURCE_FILTER: Record<MapFilterKey, boolean> = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
const DEFAULT_MAP_SOURCE_FILTER: Record<MapFilterKey, boolean> = { ais: true, vdes: true, aprs: true, hf_aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
||||||
const mapFilter: Record<MapFilterKey, boolean> = { ...DEFAULT_MAP_SOURCE_FILTER };
|
const mapFilter: Record<MapFilterKey, boolean> = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||||
/** Chip key that clears a selection rather than naming a band or a source. */
|
/** Chip key that clears a selection rather than naming a band or a source. */
|
||||||
const MAP_FILTER_ALL_KEY = "__all";
|
const MAP_FILTER_ALL_KEY = "__all";
|
||||||
@@ -302,6 +305,18 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The source a station marker belongs to, defaulting to VHF APRS for
|
||||||
|
* entries stored before HF APRS had a source of its own. */
|
||||||
|
function aprsEntrySource(entry: MapEntry | null | undefined): DecoderSource {
|
||||||
|
return entry?.type === "hf_aprs" ? "hf_aprs" : "aprs";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map key for a station: the callsign alone on VHF, source-qualified on HF,
|
||||||
|
* so one station heard on both bands keeps a marker for each. */
|
||||||
|
function aprsStationKey(call: string, source: DecoderSource): string {
|
||||||
|
return source === "aprs" ? call : `${source}:${call}`;
|
||||||
|
}
|
||||||
|
|
||||||
function refreshAprsTrack(call: string, entry: MapEntry): void {
|
function refreshAprsTrack(call: string, entry: MapEntry): void {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
|
if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
|
||||||
@@ -323,7 +338,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
interactive: false,
|
interactive: false,
|
||||||
}) as unknown as TrxLayer;
|
}) as unknown as TrxLayer;
|
||||||
track.__trxType = "aprs";
|
track.__trxType = aprsEntrySource(entry);
|
||||||
track._aprsCall = call;
|
track._aprsCall = call;
|
||||||
entry.track = track;
|
entry.track = track;
|
||||||
}
|
}
|
||||||
@@ -575,6 +590,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
|
|
||||||
function mapSourceLabel(type: DecoderSource): string {
|
function mapSourceLabel(type: DecoderSource): string {
|
||||||
if (type === "bookmark") return "Bookmarks";
|
if (type === "bookmark") return "Bookmarks";
|
||||||
|
if (type === "hf_aprs") return "HF APRS";
|
||||||
return String(type || "").toUpperCase();
|
return String(type || "").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,6 +610,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
if (type === "vdes") return "#a78bfa";
|
if (type === "vdes") return "#a78bfa";
|
||||||
if (type === "sat") return "#f59e0b";
|
if (type === "sat") return "#f59e0b";
|
||||||
if (type === "aprs") return "#00d17f";
|
if (type === "aprs") return "#00d17f";
|
||||||
|
// Far enough from the VHF green to tell the two apart at a glance.
|
||||||
|
if (type === "hf_aprs") return "#fb7185";
|
||||||
return locatorFilterColor(type);
|
return locatorFilterColor(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,10 +1206,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const entry of stationMarkers.values()) {
|
for (const entry of stationMarkers.values()) {
|
||||||
if (entry?.type === "aprs" && entry?.visibleInHistoryWindow) {
|
if (!entry?.visibleInHistoryWindow) continue;
|
||||||
availableSources.add("aprs");
|
availableSources.add(aprsEntrySource(entry));
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const bandMap = new Map<string, FilterChip>();
|
const bandMap = new Map<string, FilterChip>();
|
||||||
for (const entry of locatorMarkers.values()) {
|
for (const entry of locatorMarkers.values()) {
|
||||||
@@ -1225,7 +1241,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceItems: FilterChip[] = (["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"] as DecoderSource[])
|
const sourceItems: FilterChip[] = (["ais", "vdes", "aprs", "hf_aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"] as DecoderSource[])
|
||||||
.filter((key) => availableSources.has(key))
|
.filter((key) => availableSources.has(key))
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
key,
|
key,
|
||||||
@@ -1296,9 +1312,10 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
}
|
}
|
||||||
return parts.join(" ").toLowerCase();
|
return parts.join(" ").toLowerCase();
|
||||||
}
|
}
|
||||||
if (type === "aprs") {
|
if (type === "aprs" || type === "hf_aprs") {
|
||||||
const call = marker?._aprsCall ? String(marker._aprsCall) : "";
|
const key = marker?._aprsCall ? String(marker._aprsCall) : "";
|
||||||
const entry = stationMarkers.get(call);
|
const entry = stationMarkers.get(key);
|
||||||
|
const call = entry?.call ?? key;
|
||||||
const info = entry?.info ? String(entry.info) : "";
|
const info = entry?.info ? String(entry.info) : "";
|
||||||
const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
|
const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
|
||||||
return `${call} ${info} ${pktRaw}`.toLowerCase();
|
return `${call} ${info} ${pktRaw}`.toLowerCase();
|
||||||
@@ -1496,9 +1513,10 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
};
|
};
|
||||||
|
|
||||||
mapWindow.clearMapMarkersByType = function(type) {
|
mapWindow.clearMapMarkersByType = function(type) {
|
||||||
if (type === "aprs") {
|
if (type === "aprs" || type === "hf_aprs") {
|
||||||
selectedAprsTrackCall = null;
|
selectedAprsTrackCall = null;
|
||||||
stationMarkers.forEach((entry) => {
|
stationMarkers.forEach((entry, key) => {
|
||||||
|
if (aprsEntrySource(entry) !== type) return;
|
||||||
if (entry && entry.marker) {
|
if (entry && entry.marker) {
|
||||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.marker);
|
mapMarkers.delete(entry.marker);
|
||||||
@@ -1507,8 +1525,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.track);
|
mapMarkers.delete(entry.track);
|
||||||
}
|
}
|
||||||
|
stationMarkers.delete(key);
|
||||||
});
|
});
|
||||||
stationMarkers.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1781,13 +1799,15 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
if (!ll) return;
|
if (!ll) return;
|
||||||
const entry = stationMarkers.get(marker._aprsCall);
|
const entry = stationMarkers.get(marker._aprsCall);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
const source = aprsEntrySource(entry);
|
||||||
|
const call = entry.call ?? String(marker._aprsCall);
|
||||||
|
e.popup.setContent(buildAprsPopupHtml(call, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
||||||
refreshAprsTrack(String(marker._aprsCall), entry);
|
refreshAprsTrack(String(marker._aprsCall), entry);
|
||||||
if (entry.track && aprsMap && mapFilter.aprs && !aprsMap.hasLayer(entry.track)) {
|
if (entry.track && aprsMap && mapFilter[source] && !aprsMap.hasLayer(entry.track)) {
|
||||||
entry.track.addTo(aprsMap);
|
entry.track.addTo(aprsMap);
|
||||||
}
|
}
|
||||||
selectedAprsTrackCall = String(marker._aprsCall);
|
selectedAprsTrackCall = String(marker._aprsCall);
|
||||||
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path", marker.__trxRigIds);
|
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor(source), "aprs-radio-path", marker.__trxRigIds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2270,28 +2290,33 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _aprsAddMarkerToMap(call: string, entry: MapEntry): void {
|
function _aprsAddMarkerToMap(key: string, entry: MapEntry): void {
|
||||||
if (!aprsMap || entry.lat == null || entry.lon == null) return;
|
if (!aprsMap || entry.lat == null || entry.lon == null) return;
|
||||||
refreshAprsTrack(call, entry);
|
refreshAprsTrack(key, entry);
|
||||||
|
const source = aprsEntrySource(entry);
|
||||||
|
const call = entry.call ?? key;
|
||||||
const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
|
const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
|
||||||
const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
|
const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
|
||||||
|
const color = mapSourceColor(source);
|
||||||
const marker = (icon
|
const marker = (icon
|
||||||
? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent)
|
? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent)
|
||||||
: L.circleMarker([entry.lat, entry.lon], {
|
: L.circleMarker([entry.lat, entry.lon], {
|
||||||
radius: 6, color: "#00d17f", fillColor: "#00d17f", fillOpacity: 0.8
|
radius: 6, color, fillColor: color, fillOpacity: 0.8
|
||||||
}).addTo(aprsMap).bindPopup(popupContent)) as unknown as TrxLayer;
|
}).addTo(aprsMap).bindPopup(popupContent)) as unknown as TrxLayer;
|
||||||
marker.__trxType = "aprs";
|
marker.__trxType = source;
|
||||||
marker.__trxRigIds = entry.rigIds || new Set();
|
marker.__trxRigIds = entry.rigIds || new Set();
|
||||||
marker._aprsCall = call;
|
marker._aprsCall = key;
|
||||||
entry.marker = marker;
|
entry.marker = marker;
|
||||||
mapMarkers.add(marker);
|
mapMarkers.add(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt) {
|
mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt, source) {
|
||||||
const nextPoint: Leaflet.LatLngTuple = [lat, lon];
|
const nextPoint: Leaflet.LatLngTuple = [lat, lon];
|
||||||
const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
|
const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
|
||||||
const msgRigId = pkt?.rig_id || T.lastActiveRigId;
|
const msgRigId = pkt?.rig_id || T.lastActiveRigId;
|
||||||
const existing = stationMarkers.get(call);
|
const stationSource: DecoderSource = source === "hf_aprs" ? "hf_aprs" : "aprs";
|
||||||
|
const key = aprsStationKey(call, stationSource);
|
||||||
|
const existing = stationMarkers.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.pkt = pkt;
|
existing.pkt = pkt;
|
||||||
existing.lat = lat;
|
existing.lat = lat;
|
||||||
@@ -2310,7 +2335,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
} else if (prevPoint) {
|
} else if (prevPoint) {
|
||||||
prevPoint.tsMs = tsMs;
|
prevPoint.tsMs = tsMs;
|
||||||
}
|
}
|
||||||
pruneAprsEntry(call, existing, mapHistoryCutoffMs());
|
existing.call = call;
|
||||||
|
pruneAprsEntry(key, existing, mapHistoryCutoffMs());
|
||||||
if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
|
if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
|
||||||
existing.marker.setLatLng([lat, lon]);
|
existing.marker.setLatLng([lat, lon]);
|
||||||
existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
|
existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
|
||||||
@@ -2321,7 +2347,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
track: null,
|
track: null,
|
||||||
trackHistory: [{ lat, lon, tsMs }],
|
trackHistory: [{ lat, lon, tsMs }],
|
||||||
trackPoints: [nextPoint],
|
trackPoints: [nextPoint],
|
||||||
type: "aprs",
|
type: stationSource,
|
||||||
|
call,
|
||||||
pkt,
|
pkt,
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
@@ -2330,9 +2357,9 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
symbolCode,
|
symbolCode,
|
||||||
rigIds: new Set(msgRigId ? [msgRigId] : []),
|
rigIds: new Set(msgRigId ? [msgRigId] : []),
|
||||||
};
|
};
|
||||||
stationMarkers.set(call, entry);
|
stationMarkers.set(key, entry);
|
||||||
pruneAprsEntry(call, entry, mapHistoryCutoffMs());
|
pruneAprsEntry(key, entry, mapHistoryCutoffMs());
|
||||||
if (entry.visibleInHistoryWindow) ensureAprsMarker(call, entry);
|
if (entry.visibleInHistoryWindow) ensureAprsMarker(key, entry);
|
||||||
if (aprsMap) scheduleDecodeMapMaintenance();
|
if (aprsMap) scheduleDecodeMapMaintenance();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -242,19 +242,28 @@ export function initializeFtxDecoder(config: FtxConfig): void {
|
|||||||
}
|
}
|
||||||
messagesElement.replaceChildren(fragment);
|
messagesElement.replaceChildren(fragment);
|
||||||
};
|
};
|
||||||
const normalize = (message: FtxMessage): FtxMessage => {
|
/** Hands a decode's grid squares to the map, if the map module is loaded yet.
|
||||||
|
* Split out of normalize so a decode can be replayed onto a map that
|
||||||
|
* arrived later: the module is lazy, and everything decoded before it
|
||||||
|
* loaded had nowhere to go. */
|
||||||
|
const plotLocator = (message: FtxMessage): void => {
|
||||||
const raw = message.message ?? "";
|
const raw = message.message ?? "";
|
||||||
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
|
||||||
const grids = locatorDetails.length > 0
|
const grids = locatorDetails.length > 0
|
||||||
? locatorDetails.map(({ grid }) => grid)
|
? locatorDetails.map(({ grid }) => grid)
|
||||||
: bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
: bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
|
||||||
|
if (grids.length === 0) return;
|
||||||
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
|
||||||
|
// Already an RF frequency on a replay, an audio offset on arrival; the
|
||||||
|
// conversion only fires below 100 kHz, so it is right either way.
|
||||||
const frequency = displayFrequency(message.freq_hz);
|
const frequency = displayFrequency(message.freq_hz);
|
||||||
if (grids.length > 0) {
|
bridge.mapAddLocator?.(raw, grids, id, station, {
|
||||||
bridge.mapAddLocator?.(raw, grids, id, station, {
|
...message, freq_hz: frequency ?? message.freq_hz, locator_details: locatorDetails,
|
||||||
...message, freq_hz: frequency ?? message.freq_hz, locator_details: locatorDetails,
|
});
|
||||||
});
|
};
|
||||||
}
|
const normalize = (message: FtxMessage): FtxMessage => {
|
||||||
|
const frequency = displayFrequency(message.freq_hz);
|
||||||
|
plotLocator(message);
|
||||||
return {
|
return {
|
||||||
// The rig that heard it, kept so the mini view can tell a decode of the
|
// The rig that heard it, kept so the mini view can tell a decode of the
|
||||||
// rig on screen from one a background rig made on another band.
|
// rig on screen from one a background rig made on another band.
|
||||||
@@ -312,6 +321,8 @@ export function initializeFtxDecoder(config: FtxConfig): void {
|
|||||||
prune: () => { prune(); render(); },
|
prune: () => { prune(); render(); },
|
||||||
reset,
|
reset,
|
||||||
rerender: () => { bridge.updateFt8Bar?.(); render(); },
|
rerender: () => { bridge.updateFt8Bar?.(); render(); },
|
||||||
|
// Oldest first, so the map builds the grids up in the order they were heard.
|
||||||
|
syncMap: () => { for (const message of [...history].reverse()) plotLocator(message); },
|
||||||
});
|
});
|
||||||
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ interface HfAprsBridge {
|
|||||||
getDecodeHistoryRetentionMs?: () => number;
|
getDecodeHistoryRetentionMs?: () => number;
|
||||||
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
|
||||||
navigateToAprsMap?: (lat: number, lon: number) => void;
|
navigateToAprsMap?: (lat: number, lon: number) => void;
|
||||||
|
aprsMapAddStation?: (
|
||||||
|
call: string, lat: number, lon: number, info: string,
|
||||||
|
symbolTable: string | null | undefined, symbolCode: string | null | undefined,
|
||||||
|
packet: AprsPacket, source: "aprs" | "hf_aprs",
|
||||||
|
) => void;
|
||||||
|
clearMapMarkersByType?: (type: string) => void;
|
||||||
getDecodeRigMeta?: () => unknown;
|
getDecodeRigMeta?: () => unknown;
|
||||||
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
|
||||||
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
|
||||||
@@ -177,6 +183,7 @@ function resetHfAprsHistoryView(): void {
|
|||||||
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
|
||||||
hfAprsPacketHistory = [];
|
hfAprsPacketHistory = [];
|
||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
|
hfAprsWindow.clearMapMarkersByType?.("hf_aprs");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pruneHfAprsHistoryView(): void {
|
function pruneHfAprsHistoryView(): void {
|
||||||
@@ -184,6 +191,17 @@ function pruneHfAprsHistoryView(): void {
|
|||||||
renderHfAprsHistory();
|
renderHfAprsHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hands a positioned packet to the map, if the map module is loaded yet.
|
||||||
|
* HF traffic goes on as its own source: it is a different band and a
|
||||||
|
* different path, and the map filter offers it separately. */
|
||||||
|
function plotHfAprsPacket(pkt: AprsPacket): void {
|
||||||
|
if (pkt.lat == null || pkt.lon == null || !hfAprsWindow.aprsMapAddStation) return;
|
||||||
|
hfAprsWindow.aprsMapAddStation(
|
||||||
|
pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "",
|
||||||
|
pkt.symbolTable, pkt.symbolCode, pkt, "hf_aprs",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function addHfAprsPacket(pkt: AprsPacket): void {
|
function addHfAprsPacket(pkt: AprsPacket): void {
|
||||||
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
|
||||||
pkt._tsMs = tsMs;
|
pkt._tsMs = tsMs;
|
||||||
@@ -192,6 +210,8 @@ function addHfAprsPacket(pkt: AprsPacket): void {
|
|||||||
hfAprsPacketHistory.unshift(pkt);
|
hfAprsPacketHistory.unshift(pkt);
|
||||||
pruneHfAprsPacketHistory();
|
pruneHfAprsPacketHistory();
|
||||||
|
|
||||||
|
plotHfAprsPacket(pkt);
|
||||||
|
|
||||||
scheduleHfAprsHistoryRender();
|
scheduleHfAprsHistoryRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +229,7 @@ function onServerHfAprsBatch(packets: AprsPacket[]): void {
|
|||||||
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
|
||||||
next._tsMs = tsMs;
|
next._tsMs = tsMs;
|
||||||
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
|
plotHfAprsPacket(next);
|
||||||
normalized.push(next);
|
normalized.push(next);
|
||||||
}
|
}
|
||||||
normalized.reverse();
|
normalized.reverse();
|
||||||
@@ -290,4 +311,6 @@ renderHfAprsHistory();
|
|||||||
reset: resetHfAprsHistoryView,
|
reset: resetHfAprsHistoryView,
|
||||||
prune: pruneHfAprsHistoryView,
|
prune: pruneHfAprsHistoryView,
|
||||||
rerender: renderHfAprsHistory,
|
rerender: renderHfAprsHistory,
|
||||||
|
// Oldest first, so station tracks are rebuilt in the order they happened.
|
||||||
|
syncMap: () => { for (const entry of [...hfAprsPacketHistory].reverse()) plotHfAprsPacket(entry); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ interface WsprMessage {
|
|||||||
dt_s?: number | undefined;
|
dt_s?: number | undefined;
|
||||||
freq_hz?: number | undefined;
|
freq_hz?: number | undefined;
|
||||||
rig_id?: string | null | undefined;
|
rig_id?: string | null | undefined;
|
||||||
|
/** RF frequency the spot was heard on, kept so a map replay does not
|
||||||
|
* recompute it against wherever the dial has moved to since. */
|
||||||
|
_rfHz?: number | null | undefined;
|
||||||
receiver?: unknown;
|
receiver?: unknown;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -135,6 +138,7 @@ function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: str
|
|||||||
rfHz,
|
rfHz,
|
||||||
history: {
|
history: {
|
||||||
rig_id: msg.rig_id ?? null,
|
rig_id: msg.rig_id ?? null,
|
||||||
|
_rfHz: rfHz,
|
||||||
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
|
||||||
ts_ms: msg.ts_ms,
|
ts_ms: msg.ts_ms,
|
||||||
snr_db: msg.snr_db,
|
snr_db: msg.snr_db,
|
||||||
@@ -152,12 +156,7 @@ function onServerWsprBatch(messages: WsprMessage[]): void {
|
|||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
// "Receiving" is the panel's own status, and the panel is the rig's.
|
// "Receiving" is the panel's own status, and the panel is the rig's.
|
||||||
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
plotWsprLocator(msg);
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
|
||||||
...msg,
|
|
||||||
...(next.rfHz === null ? {} : { freq_hz: next.rfHz }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
|
||||||
normalized.push(next.history);
|
normalized.push(next.history);
|
||||||
}
|
}
|
||||||
@@ -317,15 +316,24 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Hands a spot's grid squares to the map, if the map module is loaded yet.
|
||||||
|
* The module is lazy, so a spot heard before it arrived has to be replayable. */
|
||||||
|
function plotWsprLocator(msg: WsprMessage): void {
|
||||||
|
const next = normalizeServerWsprMessage(msg);
|
||||||
|
if (next.grids.length === 0 || !wsprWindow.mapAddLocator) return;
|
||||||
|
// A replayed spot carries the frequency it was heard on; a fresh one has it
|
||||||
|
// worked out from the dial it just arrived against.
|
||||||
|
const rfHz = finiteNumber(msg._rfHz) ?? next.rfHz;
|
||||||
|
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
||||||
|
...msg,
|
||||||
|
...(rfHz === null ? {} : { freq_hz: rfHz }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onServerWspr(msg: WsprMessage): void {
|
function onServerWspr(msg: WsprMessage): void {
|
||||||
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
if (wsprStatus && isActiveRigDecode(msg.rig_id)) wsprStatus.textContent = "Receiving";
|
||||||
const next = normalizeServerWsprMessage(msg);
|
const next = normalizeServerWsprMessage(msg);
|
||||||
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
|
plotWsprLocator(msg);
|
||||||
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
|
|
||||||
...msg,
|
|
||||||
...(next.rfHz === null ? {} : { freq_hz: next.rfHz }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addWsprMessage(next.history);
|
addWsprMessage(next.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,4 +345,6 @@ wsprWindow.trxPluginRuntime.registerDecoder({
|
|||||||
prune: pruneWsprHistoryView,
|
prune: pruneWsprHistoryView,
|
||||||
reset: resetWsprHistoryView,
|
reset: resetWsprHistoryView,
|
||||||
rerender: renderWsprHistory,
|
rerender: renderWsprHistory,
|
||||||
|
// Oldest first, so the map builds the grids up in the order they were heard.
|
||||||
|
syncMap: () => { for (const message of [...wsprMessageHistory].reverse()) plotWsprLocator(message); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ const BEACON = {
|
|||||||
// A second rig listening in the background, on its own band. Both pages
|
// A second rig listening in the background, on its own band. Both pages
|
||||||
// describe the rig on screen, so its traffic belongs in neither the panel nor
|
// describe the rig on screen, so its traffic belongs in neither the panel nor
|
||||||
// the mini view — only on the map, which shows the whole station.
|
// the mini view — only on the map, which shows the whole station.
|
||||||
|
// HF APRS travels a different band and a different path from the VHF list, so
|
||||||
|
// it goes on the map under a source of its own that the filter can hide.
|
||||||
|
const HF_BEACON = {
|
||||||
|
type: "hf_aprs", src_call: "SP5HF-7", dest_call: "APRS", path: "WIDE2-2", info: "HF beacon",
|
||||||
|
packet_type: "position", crc_ok: true, lat: 50.06, lon: 19.94,
|
||||||
|
symbol_table: "/", symbol_code: ">", rig_id: "rig-a",
|
||||||
|
};
|
||||||
const OTHER_RIG_VESSEL = {
|
const OTHER_RIG_VESSEL = {
|
||||||
...VESSEL, mmsi: 244660001, vessel_name: "ELDERBERRY", callsign: "PBTY",
|
...VESSEL, mmsi: 244660001, vessel_name: "ELDERBERRY", callsign: "PBTY",
|
||||||
lat: 51.92, lon: 4.48, rig_id: "rig-b",
|
lat: 51.92, lon: 4.48, rig_id: "rig-b",
|
||||||
@@ -32,7 +39,7 @@ const OTHER_RIG_VESSEL = {
|
|||||||
|
|
||||||
// AIS is what the mini view for vessels is gated on; the rig has to be on it.
|
// AIS is what the mini view for vessels is gated on; the rig has to be on it.
|
||||||
const fixture = await startWebFixture({
|
const fixture = await startWebFixture({
|
||||||
spectrum: true, decodes: [VESSEL, BEACON, OTHER_RIG_VESSEL], mode: "AIS",
|
spectrum: true, decodes: [VESSEL, BEACON, HF_BEACON, OTHER_RIG_VESSEL], mode: "AIS",
|
||||||
});
|
});
|
||||||
const { browser, page, runtimeErrors } = await startBrowser(chromium);
|
const { browser, page, runtimeErrors } = await startBrowser(chromium);
|
||||||
|
|
||||||
@@ -104,6 +111,36 @@ try {
|
|||||||
return { ais: size(map?.aisMarkers), stations: size(map?.stationMarkers) };
|
return { ais: size(map?.aisMarkers), stations: size(map?.stationMarkers) };
|
||||||
});
|
});
|
||||||
assert.ok(markers.stations > 0, "the APRS station never reached the map");
|
assert.ok(markers.stations > 0, "the APRS station never reached the map");
|
||||||
|
// Both APRS stations are on it, each under its own source: the HF one used
|
||||||
|
// to be dropped entirely, since the HF list never plotted anything.
|
||||||
|
const aprsSources = await page.evaluate(() => {
|
||||||
|
const entries = [...(window.trx.modules.map?.stationMarkers ?? new Map()).entries()];
|
||||||
|
return entries.map(([key, entry]) => `${entry?.type ?? "?"}:${entry?.call ?? key}`).sort();
|
||||||
|
});
|
||||||
|
assert.deepEqual(aprsSources, ["aprs:SP2SJG-9", "hf_aprs:SP5HF-7"],
|
||||||
|
`the map holds ${JSON.stringify(aprsSources)}`);
|
||||||
|
// And the filter offers HF APRS on a chip of its own, next to the VHF one.
|
||||||
|
await page.locator('#map-locator-phase .map-locator-phase-btn[data-phase="type"]').click();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
const chips = await page.evaluate(() => [...document.querySelectorAll("#map-locator-choice-filter .map-locator-chip")]
|
||||||
|
.map((chip) => chip.dataset.filterKey));
|
||||||
|
assert.ok(chips.includes("aprs") && chips.includes("hf_aprs"),
|
||||||
|
`the Show chips are ${JSON.stringify(chips)}`);
|
||||||
|
|
||||||
|
// Turning that chip off takes the HF station off the map and leaves the VHF
|
||||||
|
// one on: the two are filtered apart, which is the whole point of the split.
|
||||||
|
await page.locator('#map-locator-choice-filter .map-locator-chip[data-filter-key="hf_aprs"]').click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const shownAfterFilter = await page.evaluate(() => {
|
||||||
|
const map = window.trx.modules.map;
|
||||||
|
const onMap = [];
|
||||||
|
(map?.stationMarkers ?? new Map()).forEach((entry, key) => {
|
||||||
|
if (entry?.marker && map.aprsMap?.hasLayer(entry.marker)) onMap.push(entry?.call ?? key);
|
||||||
|
});
|
||||||
|
return onMap.sort();
|
||||||
|
});
|
||||||
|
assert.deepEqual(shownAfterFilter, ["SP2SJG-9"],
|
||||||
|
`hiding HF APRS left ${JSON.stringify(shownAfterFilter)} on the map`);
|
||||||
// The map is the whole station's view — the one place a background rig's
|
// The map is the whole station's view — the one place a background rig's
|
||||||
// traffic belongs — so both vessels are on it even though the panel and the
|
// traffic belongs — so both vessels are on it even though the panel and the
|
||||||
// mini view show only the selected rig's.
|
// mini view show only the selected rig's.
|
||||||
@@ -120,6 +157,8 @@ try {
|
|||||||
// left the client on its retry path and the history path untested.
|
// left the client on its retry path and the history path untested.
|
||||||
const HISTORY_AIS = 900;
|
const HISTORY_AIS = 900;
|
||||||
const HISTORY_APRS = 300;
|
const HISTORY_APRS = 300;
|
||||||
|
const HISTORY_FT8 = 12;
|
||||||
|
const HISTORY_WSPR = 8;
|
||||||
const historyFixture = await startWebFixture({
|
const historyFixture = await startWebFixture({
|
||||||
spectrum: true,
|
spectrum: true,
|
||||||
mode: "AIS",
|
mode: "AIS",
|
||||||
@@ -129,6 +168,14 @@ const historyFixture = await startWebFixture({
|
|||||||
vessel_name: `HISTORIC ${index}`, channel: "A", message_type: 1,
|
vessel_name: `HISTORIC ${index}`, channel: "A", message_type: 1,
|
||||||
rig_id: "rig-a", ts_ms: Date.now() - (index + 1) * 1000,
|
rig_id: "rig-a", ts_ms: Date.now() - (index + 1) * 1000,
|
||||||
})),
|
})),
|
||||||
|
ft8: Array.from({ length: HISTORY_FT8 }, (_, index) => ({
|
||||||
|
message: `CQ SP${index}ABC JO${String(index).padStart(2, "0")}`, snr_db: -7, dt_s: 0.2,
|
||||||
|
freq_hz: 1200 + index, rig_id: "rig-a", ts_ms: Date.now() - (index + 1) * 1000,
|
||||||
|
})),
|
||||||
|
wspr: Array.from({ length: HISTORY_WSPR }, (_, index) => ({
|
||||||
|
message: `SP${index}XYZ JN${String(index).padStart(2, "0")} 30`, snr_db: -22, dt_s: 0.5,
|
||||||
|
freq_hz: 1500 + index, rig_id: "rig-a", ts_ms: Date.now() - (index + 1) * 1000,
|
||||||
|
})),
|
||||||
aprs: Array.from({ length: HISTORY_APRS }, (_, index) => ({
|
aprs: Array.from({ length: HISTORY_APRS }, (_, index) => ({
|
||||||
src_call: `SP2SJG-${index % 15}`, dest_call: "APRS", path: "WIDE1-1",
|
src_call: `SP2SJG-${index % 15}`, dest_call: "APRS", path: "WIDE1-1",
|
||||||
info: `history ${index}`, packet_type: "position", crc_ok: true,
|
info: `history ${index}`, packet_type: "position", crc_ok: true,
|
||||||
@@ -216,6 +263,27 @@ try {
|
|||||||
assert.equal(plotted.ais, HISTORY_AIS, `${plotted.ais} of ${HISTORY_AIS} vessels reached the map`);
|
assert.equal(plotted.ais, HISTORY_AIS, `${plotted.ais} of ${HISTORY_AIS} vessels reached the map`);
|
||||||
assert.equal(plotted.stations, 15, `${plotted.stations} of 15 stations reached the map`);
|
assert.equal(plotted.stations, 15, `${plotted.stations} of 15 stations reached the map`);
|
||||||
|
|
||||||
|
// The Statistics page counts the same history. Its decode log lives in the
|
||||||
|
// map module, which is lazy, so every decode that arrived before the module
|
||||||
|
// did used to be recorded into nothing at all — the page opened empty and
|
||||||
|
// only filled in from decodes heard afterwards, and it took a reload landing
|
||||||
|
// on the tab (module loaded at startup, before the history) to show the lot.
|
||||||
|
await replay.page.evaluate(() => window.navigateToTab("statistics"));
|
||||||
|
await replay.page.waitForTimeout(1500);
|
||||||
|
// The counters are written with toLocaleString(), so a four-figure count
|
||||||
|
// arrives as "1,220" — whichever separator the runner's locale picks. Read
|
||||||
|
// the digits rather than the formatting.
|
||||||
|
const counted = await replay.page.evaluate(() => {
|
||||||
|
const count = (id) => Number((document.getElementById(id)?.textContent ?? "").replace(/\D/g, ""));
|
||||||
|
return { decodes: count("stats-total-decodes"), grids: count("stats-unique-grids") };
|
||||||
|
});
|
||||||
|
assert.equal(counted.decodes, HISTORY_AIS + HISTORY_APRS + HISTORY_FT8 + HISTORY_WSPR,
|
||||||
|
`the statistics counted ${counted.decodes} decodes`);
|
||||||
|
// Grid squares come from the FT8 and WSPR spots, which had no map replay of
|
||||||
|
// their own: the locators of everything heard before the map loaded were lost.
|
||||||
|
assert.equal(counted.grids, HISTORY_FT8 + HISTORY_WSPR,
|
||||||
|
`the statistics counted ${counted.grids} grid squares`);
|
||||||
|
|
||||||
assert.deepEqual(replay.runtimeErrors, []);
|
assert.deepEqual(replay.runtimeErrors, []);
|
||||||
} finally {
|
} finally {
|
||||||
await replay.browser.close();
|
await replay.browser.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user