[feat](trx-rs): add weather satellite map overlay integration

Add SGP4-based geo-referencing for NOAA APT and Meteor LRPT decoded
satellite images, enabling them to be displayed as semi-transparent
overlays on the Leaflet map module with ground track polylines.

Changes:
- Add sgp4 crate dependency to trx-core for orbital propagation
- New trx-core/src/geo.rs module with TLE-based pass geo-referencing,
  ECI-to-geodetic conversion, and station-location fallback estimation
- Extend WxsatImage and LrptImage structs with geo_bounds and
  ground_track optional fields (backward compatible via serde defaults)
- Compute geo-bounds in finalize_wxsat_pass and finalize_lrpt_pass
  using satellite identity, pass timestamps, and station coordinates
- Add 'wxsat' source filter to the map module (off by default)
- Add L.imageOverlay rendering with popup and ground track polyline
- Add "Show on Map" buttons in wxsat plugin live/history views

https://claude.ai/code/session_01DUCfb9CjGoViwBrznpfWyt
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 10:57:08 +00:00
committed by Stan Grams
parent c1b713a5b2
commit 560b6ec912
9 changed files with 757 additions and 4 deletions
@@ -4489,7 +4489,7 @@ const locatorMarkers = new Map();
const decodeContactPaths = new Map();
let selectedMapQsoKey = null;
const mapMarkers = new Set();
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true };
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, wxsat: false };
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
const mapLocatorFilter = { phase: "band", bands: new Set() };
let mapSearchFilter = "";
@@ -4861,6 +4861,7 @@ function locatorFilterColor(type) {
function mapSourceColor(type) {
if (type === "ais") return "#38bdf8";
if (type === "vdes") return "#a78bfa";
if (type === "wxsat") return "#f59e0b";
if (type === "aprs") return "#00d17f";
return locatorFilterColor(type);
}
@@ -5426,6 +5427,14 @@ function renderMapLocatorLegend(phase, sourceItems, bandItems) {
legendEl.innerHTML = `<div class="map-band-legend-title">${title}</div><div class="map-band-legend-list">${rows}</div>`;
}
window.enableMapSourceFilter = function(key) {
if (Object.prototype.hasOwnProperty.call(mapFilter, key) && !mapFilter[key]) {
mapFilter[key] = true;
rebuildMapLocatorFilters();
applyMapFilter();
}
};
function rebuildMapLocatorFilters() {
const phaseEl = document.getElementById("map-locator-phase");
const choiceEl = document.getElementById("map-locator-choice-filter");
@@ -5663,6 +5672,95 @@ function syncAprsReceiverMarker() {
if (!seen.size) aprsMapReceiverMarker = null;
}
// ---------------------------------------------------------------------------
// Weather satellite image overlays on the map
// ---------------------------------------------------------------------------
const wxsatOverlays = new Map(); // key -> { overlay, track, msg }
let wxsatOverlaySeq = 0;
window.addWxsatMapOverlay = function(msg) {
if (!msg || !msg.geo_bounds || !msg.path) return;
const bounds = msg.geo_bounds;
// bounds = [south, west, north, east]
if (!Array.isArray(bounds) || bounds.length !== 4) return;
const latLngBounds = L.latLngBounds(
[bounds[0], bounds[1]], // SW
[bounds[2], bounds[3]] // NE
);
const key = "wxsat-" + (++wxsatOverlaySeq);
const overlay = L.imageOverlay(msg.path, latLngBounds, {
opacity: 0.55,
interactive: true,
zIndex: 300,
});
overlay.__trxType = "wxsat";
overlay.__trxWxsatKey = key;
overlay.__trxRigIds = msg.rig_id ? new Set([msg.rig_id]) : new Set();
overlay.__trxHistoryVisible = true;
mapMarkers.add(overlay);
// Build a popup for the overlay
const decoder = msg.mcu_count != null ? "Meteor LRPT" : "NOAA APT";
const satellite = msg.satellite || "Unknown";
const ts = msg.ts_ms ? new Date(msg.ts_ms).toLocaleString() : "";
overlay.bindPopup(
`<div style="font-size:0.82rem;max-width:200px;">` +
`<strong>${escapeMapHtml(decoder)}</strong><br>` +
`${escapeMapHtml(satellite)}<br>` +
`${escapeMapHtml(ts)}<br>` +
(msg.path ? `<a href="${escapeMapHtml(msg.path)}" target="_blank" style="color:var(--accent);">Download PNG</a>` : "") +
`</div>`
);
// Add ground track polyline if available
let track = null;
if (msg.ground_track && Array.isArray(msg.ground_track) && msg.ground_track.length >= 2) {
const latlngs = msg.ground_track.map(function(pt) { return [pt[0], pt[1]]; });
track = L.polyline(latlngs, {
color: mapSourceColor("wxsat"),
weight: 2,
opacity: 0.7,
dashArray: "6, 4",
});
track.__trxType = "wxsat";
track.__trxWxsatKey = key;
track.__trxRigIds = overlay.__trxRigIds;
track.__trxHistoryVisible = true;
mapMarkers.add(track);
if (aprsMap) {
track.addTo(aprsMap);
}
}
wxsatOverlays.set(key, { overlay: overlay, track: track, msg: msg });
if (aprsMap) {
overlay.addTo(aprsMap);
}
applyMapFilter();
};
window.removeWxsatMapOverlay = function(key) {
const entry = wxsatOverlays.get(key);
if (!entry) return;
if (entry.overlay) {
mapMarkers.delete(entry.overlay);
if (aprsMap && aprsMap.hasLayer(entry.overlay)) entry.overlay.removeFrom(aprsMap);
}
if (entry.track) {
mapMarkers.delete(entry.track);
if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
}
wxsatOverlays.delete(key);
};
window.clearWxsatMapOverlays = function() {
for (const [key] of wxsatOverlays) {
window.removeWxsatMapOverlay(key);
}
};
window.clearMapMarkersByType = function(type) {
if (type === "aprs") {
selectedAprsTrackCall = null;
@@ -5707,6 +5805,11 @@ window.clearMapMarkersByType = function(type) {
return;
}
if (type === "wxsat") {
window.clearWxsatMapOverlays();
return;
}
if (type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
const prefix = `${type}:`;
for (const [key, entry] of locatorMarkers.entries()) {
@@ -95,6 +95,9 @@ function renderWxsatLatestCard() {
if (img.path) {
html += `<a href="${img.path}" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">Download PNG</a>`;
}
if (img.geo_bounds) {
html += ` <button type="button" class="wxsat-map-btn" onclick="window.wxsatShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`;
}
html += `</div>`;
wxsatLiveLatest.innerHTML = html;
}
@@ -146,9 +149,12 @@ function renderWxsatHistoryRow(img) {
const channels = decoder === "lrpt" ? (img.channels || "--") : (img.channel_a && img.channel_b ? `A:${img.channel_a} B:${img.channel_b}` : img.channel_a || "--");
const lines = img.line_count || img.mcu_count || 0;
const unit = decoder === "lrpt" ? "MCU" : "ln";
const link = img.path
let link = img.path
? `<a href="${img.path}" target="_blank" style="color:var(--accent);">PNG</a>`
: "--";
if (img.geo_bounds) {
link += ` <a href="javascript:void(0)" onclick="window.wxsatShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="color:var(--accent);">Map</a>`;
}
row.innerHTML = [
`<span>${date} ${ts}</span>`,
@@ -209,11 +215,17 @@ function addWxsatImage(img, decoder) {
window.onServerWxsatImage = function (msg) {
if (wxsatStatus) wxsatStatus.textContent = "Image received (NOAA APT)";
addWxsatImage(msg, "apt");
if (msg.geo_bounds && msg.path && window.addWxsatMapOverlay) {
window.addWxsatMapOverlay(msg);
}
};
window.onServerLrptImage = function (msg) {
if (wxsatStatus) wxsatStatus.textContent = "Image received (Meteor LRPT)";
addWxsatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && window.addWxsatMapOverlay) {
window.addWxsatMapOverlay(msg);
}
};
window.resetWxsatHistoryView = function () {
@@ -221,6 +233,7 @@ window.resetWxsatHistoryView = function () {
if (wxsatHistoryList) wxsatHistoryList.innerHTML = "";
renderWxsatLatestCard();
renderWxsatHistoryTable();
if (window.clearWxsatMapOverlays) window.clearWxsatMapOverlays();
};
window.pruneWxsatHistoryView = function () {
@@ -271,6 +284,20 @@ document
}
});
// ── Navigate to map centered on satellite image bounds ──────────────
window.wxsatShowOnMap = function (south, west, north, east) {
// Enable wxsat filter if not active
if (typeof window.enableMapSourceFilter === "function") {
window.enableMapSourceFilter("wxsat");
}
// Navigate to the center of the image bounds
const lat = (south + north) / 2;
const lon = (west + east) / 2;
if (window.navigateToAprsMap) {
window.navigateToAprsMap(lat, lon);
}
};
// ── Initial render ──────────────────────────────────────────────────
renderWxsatLatestCard();
renderWxsatHistoryTable();