[fix](trx-frontend-http): put HF APRS on the map, under its own source
The HF APRS list never plotted anything. Its plugin ships in the map plugin group and its packets carry positions, but nothing ever handed one to the map, so a station heard on 30 m appeared in the panel and nowhere else — and unlike the replay gaps around it, no reload brought it back. Plot it, and not as more VHF APRS. HF is a different band and a different path, and lumping the two together would leave no way to tell them apart or to look at one without the other, so it goes on as a source of its own: its own colour, its own chip in the map's Show filter, its own entry in the source legend, and its own clear. Station entries are keyed by source and callsign rather than callsign alone, so a station worked on both bands keeps a marker for each while the popups, the search text and the tracks still show the callsign as heard. decode-flow feeds an HF beacon alongside the VHF one and checks all of it: both reach the map under their own sources, the Show row offers HF APRS next to APRS, and turning that chip off takes the HF station off the map while the VHF one stays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
+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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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); },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user