[feat](trx-frontend-http): show all rig locations on map, load decode history for all remotes
Add latitude/longitude to /rigs API response. Map now displays receiver markers for all configured rigs, de-duplicated by location. Decode history is no longer filtered to the active rig so all remotes contribute to the map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1020,6 +1020,7 @@ async function refreshRigList() {
|
||||
serverRigs = rigs;
|
||||
serverActiveRigId = data.active_remote || null;
|
||||
applyRigList(data.active_remote, rigIds, displayNames);
|
||||
if (aprsMap) syncAprsReceiverMarker();
|
||||
} catch (e) {
|
||||
// Non-fatal: SSE/status path still drives main UI.
|
||||
}
|
||||
@@ -4245,6 +4246,7 @@ window.addEventListener("resize", resizeHeaderSignalCanvas);
|
||||
let aprsMap = null;
|
||||
let aprsMapBaseLayer = null;
|
||||
let aprsMapReceiverMarker = null;
|
||||
let aprsMapReceiverMarkers = {}; // keyed by rig remote id
|
||||
let aprsRadioPath = null;
|
||||
let selectedLocatorMarker = null;
|
||||
let selectedLocatorPulseRaf = null;
|
||||
@@ -5342,32 +5344,69 @@ function markerPassesSearchFilter(marker) {
|
||||
return terms.every((term) => haystack.includes(term));
|
||||
}
|
||||
|
||||
function _receiverLocationKey(lat, lon) {
|
||||
return lat.toFixed(6) + "," + lon.toFixed(6);
|
||||
}
|
||||
|
||||
function syncAprsReceiverMarker() {
|
||||
if (!aprsMap) return;
|
||||
const hasLocation = serverLat != null && serverLon != null;
|
||||
if (!hasLocation) {
|
||||
if (aprsMapReceiverMarker && aprsMap.hasLayer(aprsMapReceiverMarker)) {
|
||||
aprsMapReceiverMarker.removeFrom(aprsMap);
|
||||
// Build unique locations from all rigs
|
||||
const locGroups = {}; // key -> { lat, lon, rigs: [...] }
|
||||
const activeId = lastActiveRigId || serverActiveRigId || null;
|
||||
for (const rig of serverRigs) {
|
||||
if (!rig || !rig.remote) continue;
|
||||
const lat = rig.latitude, lon = rig.longitude;
|
||||
if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||
const key = _receiverLocationKey(lat, lon);
|
||||
if (!locGroups[key]) locGroups[key] = { lat, lon, rigs: [], hasActive: false };
|
||||
locGroups[key].rigs.push(rig.remote);
|
||||
if (rig.remote === activeId) locGroups[key].hasActive = true;
|
||||
}
|
||||
// Fallback: if active rig has SSE location but isn't in serverRigs yet
|
||||
if (serverLat != null && serverLon != null) {
|
||||
const key = _receiverLocationKey(serverLat, serverLon);
|
||||
if (!locGroups[key]) locGroups[key] = { lat: serverLat, lon: serverLon, rigs: [], hasActive: true };
|
||||
if (!locGroups[key].hasActive) locGroups[key].hasActive = true;
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
let didInitialView = false;
|
||||
for (const [key, group] of Object.entries(locGroups)) {
|
||||
seen.add(key);
|
||||
const latLng = [group.lat, group.lon];
|
||||
const isActive = group.hasActive;
|
||||
let m = aprsMapReceiverMarkers[key];
|
||||
if (!m) {
|
||||
m = L.circleMarker(latLng, {
|
||||
radius: isActive ? 8 : 6,
|
||||
className: "trx-receiver-marker" + (isActive ? "" : " trx-receiver-marker-secondary"),
|
||||
fillOpacity: isActive ? 0.8 : 0.6,
|
||||
}).addTo(aprsMap).bindPopup("");
|
||||
m._receiverLocKey = key;
|
||||
m._receiverRigs = group.rigs;
|
||||
aprsMapReceiverMarkers[key] = m;
|
||||
if (isActive && !didInitialView) {
|
||||
aprsMap.setView(latLng, Math.max(1, initialMapZoom));
|
||||
didInitialView = true;
|
||||
}
|
||||
} else {
|
||||
m.setLatLng(latLng);
|
||||
m._receiverRigs = group.rigs;
|
||||
m.setRadius(isActive ? 8 : 6);
|
||||
if (!aprsMap.hasLayer(m)) m.addTo(aprsMap);
|
||||
}
|
||||
aprsMapReceiverMarker = null;
|
||||
return;
|
||||
// Keep legacy reference for the active-rig location marker
|
||||
if (isActive) aprsMapReceiverMarker = m;
|
||||
}
|
||||
const latLng = [serverLat, serverLon];
|
||||
if (!aprsMapReceiverMarker) {
|
||||
aprsMapReceiverMarker = L.circleMarker(latLng, {
|
||||
radius: 8,
|
||||
className: "trx-receiver-marker",
|
||||
fillOpacity: 0.8,
|
||||
}).addTo(aprsMap).bindPopup("");
|
||||
if (typeof aprsMap.setView === "function") {
|
||||
aprsMap.setView(latLng, Math.max(1, initialMapZoom));
|
||||
// Remove markers for locations no longer present
|
||||
for (const key of Object.keys(aprsMapReceiverMarkers)) {
|
||||
if (!seen.has(key)) {
|
||||
const m = aprsMapReceiverMarkers[key];
|
||||
if (m && aprsMap.hasLayer(m)) m.removeFrom(aprsMap);
|
||||
delete aprsMapReceiverMarkers[key];
|
||||
}
|
||||
return;
|
||||
}
|
||||
aprsMapReceiverMarker.setLatLng(latLng);
|
||||
if (!aprsMap.hasLayer(aprsMapReceiverMarker)) {
|
||||
aprsMapReceiverMarker.addTo(aprsMap);
|
||||
}
|
||||
if (!seen.size) aprsMapReceiverMarker = null;
|
||||
}
|
||||
|
||||
window.clearMapMarkersByType = function(type) {
|
||||
@@ -5618,8 +5657,8 @@ function initAprsMap() {
|
||||
syncSelectedAisTrackVisibility();
|
||||
}
|
||||
|
||||
if (marker === aprsMapReceiverMarker) {
|
||||
e.popup.setContent(buildReceiverPopupHtml());
|
||||
if (marker._receiverLocKey) {
|
||||
e.popup.setContent(buildReceiverPopupHtml(marker._receiverRigs || []));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6038,7 +6077,7 @@ function formatTimeAgo(tsMs) {
|
||||
return remMins > 0 ? `${hrs}h ${remMins}min ago` : `${hrs}h ago`;
|
||||
}
|
||||
|
||||
function buildReceiverPopupHtml() {
|
||||
function buildReceiverPopupHtml(rigIds) {
|
||||
const call = serverCallsign || ownerCallsign || "Receiver";
|
||||
let meta = "";
|
||||
if (serverVersion) {
|
||||
@@ -6049,10 +6088,20 @@ function buildReceiverPopupHtml() {
|
||||
if (ownerCallsign && ownerCallsign !== serverCallsign) {
|
||||
rows += `<tr><td class="aprs-popup-label">Owner</td><td>${escapeMapHtml(ownerCallsign)}</td></tr>`;
|
||||
}
|
||||
if (serverLat != null && serverLon != null) {
|
||||
rows += `<tr><td class="aprs-popup-label">QTH</td><td>${serverLat.toFixed(5)}, ${serverLon.toFixed(5)}</td></tr>`;
|
||||
// Show location from first matching rig or active rig
|
||||
const rigSet = rigIds && rigIds.length ? new Set(rigIds) : null;
|
||||
const firstRig = rigSet ? serverRigs.find(r => rigSet.has(r.remote)) : null;
|
||||
const popupLat = firstRig ? firstRig.latitude : serverLat;
|
||||
const popupLon = firstRig ? firstRig.longitude : serverLon;
|
||||
if (popupLat != null && popupLon != null) {
|
||||
const grid = latLonToMaidenhead(popupLat, popupLon);
|
||||
rows += `<tr><td class="aprs-popup-label">QTH</td><td>${popupLat.toFixed(5)}, ${popupLon.toFixed(5)} (${escapeMapHtml(grid)})</td></tr>`;
|
||||
}
|
||||
for (const rig of serverRigs) {
|
||||
// Show rigs at this location
|
||||
const rigsToShow = rigSet
|
||||
? serverRigs.filter(r => rigSet.has(r.remote))
|
||||
: serverRigs;
|
||||
for (const rig of rigsToShow) {
|
||||
const name = rig.display_name || `${rig.manufacturer} ${rig.model}`.trim();
|
||||
const active = rig.remote === serverActiveRigId
|
||||
? ` <span class="receiver-popup-active">active</span>` : "";
|
||||
@@ -8149,9 +8198,7 @@ function scheduleDecodeHistoryDrainStep(callback) {
|
||||
}
|
||||
|
||||
function decodeHistoryUrl() {
|
||||
let url = "/decode/history";
|
||||
if (lastActiveRigId) url += "?remote=" + encodeURIComponent(lastActiveRigId);
|
||||
return url;
|
||||
return "/decode/history";
|
||||
}
|
||||
|
||||
function loadDecodeHistoryOnMainThread(onReady, onError) {
|
||||
|
||||
@@ -2152,6 +2152,7 @@ body.map-fake-fullscreen-active {
|
||||
}
|
||||
.trx-locator-selected { stroke-opacity: 1 !important; stroke-width: 3.25px !important; filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent-green) 52%, transparent)); animation: trx-locator-breathe 1.6s ease-in-out infinite; }
|
||||
.trx-receiver-marker { stroke: var(--accent-green) !important; fill: var(--accent-green) !important; }
|
||||
.trx-receiver-marker-secondary { opacity: 0.55; }
|
||||
.receiver-popup-active { font-size: 0.75em; background: rgba(194,75,26,0.15); color: var(--accent-green); border: 1px solid rgba(194,75,26,0.3); border-radius: 3px; padding: 0 0.25rem; margin-left: 0.3rem; vertical-align: middle; }
|
||||
@keyframes aprs-radio-path-flow { to { stroke-dashoffset: -15; } }
|
||||
.map-paths-static .decode-contact-path,
|
||||
|
||||
@@ -1707,6 +1707,10 @@ struct RigListItem {
|
||||
manufacturer: String,
|
||||
model: String,
|
||||
initialized: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
latitude: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
longitude: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@@ -1736,6 +1740,8 @@ fn map_rig_entry(entry: &RemoteRigEntry) -> RigListItem {
|
||||
manufacturer: entry.state.info.manufacturer.clone(),
|
||||
model: entry.state.info.model.clone(),
|
||||
initialized: entry.state.initialized,
|
||||
latitude: entry.state.server_latitude,
|
||||
longitude: entry.state.server_longitude,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user