diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 318389a..ffce0f6 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -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 += `Owner${escapeMapHtml(ownerCallsign)}`; } - if (serverLat != null && serverLon != null) { - rows += `QTH${serverLat.toFixed(5)}, ${serverLon.toFixed(5)}`; + // 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 += `QTH${popupLat.toFixed(5)}, ${popupLon.toFixed(5)} (${escapeMapHtml(grid)})`; } - 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 ? ` active` : ""; @@ -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) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 844ec53..341aff7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -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, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 6eda16c..1abe4a1 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -1707,6 +1707,10 @@ struct RigListItem { manufacturer: String, model: String, initialized: bool, + #[serde(skip_serializing_if = "Option::is_none")] + latitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + longitude: Option, } #[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, } }