[feat](trx-frontend-http): color radio paths by band or mode

Radio paths and decode contact paths now use the same color as the
marker they belong to, respecting the active filter mode:
- Band mode: color follows the band (golden-angle HSL hue)
- Mode/source mode: color follows the source type (FT8/WSPR/bookmark)

APRS, AIS, and VDES paths use their fixed source colors unchanged.
Decode contact paths sync color when the filter mode is switched.

CSS stroke/stroke-opacity removed from path classes so Leaflet's
color option takes effect; dasharray and flow animation are retained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-14 12:31:56 +01:00
parent a3c098b68f
commit b9744ef8fd
2 changed files with 29 additions and 12 deletions
@@ -4035,6 +4035,11 @@ function syncLocatorMarkerStyles() {
if (!entry?.marker) continue; if (!entry?.marker) continue;
entry.marker.setStyle(locatorStyleForEntry(entry, locatorEntryCount(entry))); entry.marker.setStyle(locatorStyleForEntry(entry, locatorEntryCount(entry)));
} }
for (const entry of decodeContactPaths.values()) {
if (!entry?.line) continue;
const color = decodeContactPathColor(entry);
entry.line.setStyle({ color, opacity: 0.78 });
}
} }
function stopSelectedLocatorPulse() { function stopSelectedLocatorPulse() {
@@ -4118,20 +4123,30 @@ function midpointLatLon(a, b) {
}; };
} }
function decodeContactPathColor(entry) {
const srcEntry = locatorMarkers.get(entry?.sourceGrid);
if (srcEntry) return locatorStyleForEntry(srcEntry, locatorEntryCount(srcEntry)).color;
return locatorFilterColor("ft8");
}
function ensureDecodeContactPathRendered(entry) { function ensureDecodeContactPathRendered(entry) {
if (!entry || !aprsMap) return; if (!entry || !aprsMap) return;
const linePoints = [ const linePoints = [
[entry.from.lat, entry.from.lon], [entry.from.lat, entry.from.lon],
[entry.to.lat, entry.to.lon], [entry.to.lat, entry.to.lon],
]; ];
const color = decodeContactPathColor(entry);
if (!entry.line) { if (!entry.line) {
entry.line = L.polyline(linePoints, { entry.line = L.polyline(linePoints, {
color,
opacity: 0.78,
className: "decode-contact-path", className: "decode-contact-path",
weight: 2.8, weight: 2.8,
interactive: false, interactive: false,
}).addTo(aprsMap); }).addTo(aprsMap);
} else { } else {
entry.line.setLatLngs(linePoints); entry.line.setLatLngs(linePoints);
entry.line.setStyle({ color, opacity: 0.78 });
if (!aprsMap.hasLayer(entry.line)) entry.line.addTo(aprsMap); if (!aprsMap.hasLayer(entry.line)) entry.line.addTo(aprsMap);
} }
const mid = midpointLatLon(entry.from, entry.to); const mid = midpointLatLon(entry.from, entry.to);
@@ -4169,14 +4184,14 @@ function syncDecodeContactPathVisibility() {
} }
} }
function setMapRadioPathTo(lat, lon, className = "aprs-radio-path") { function setMapRadioPathTo(lat, lon, color, className = "aprs-radio-path") {
clearMapRadioPath(); clearMapRadioPath();
if (!mapP2pRadioPathsEnabled || serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) { if (!mapP2pRadioPathsEnabled || serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
return; return;
} }
aprsRadioPath = L.polyline( aprsRadioPath = L.polyline(
[[serverLat, serverLon], [lat, lon]], [[serverLat, serverLon], [lat, lon]],
{ className, weight: 2, interactive: false } { color, opacity: 0.85, weight: 2, interactive: false, className }
).addTo(aprsMap); ).addTo(aprsMap);
} }
@@ -4763,7 +4778,7 @@ function initAprsMap() {
entry.track.addTo(aprsMap); entry.track.addTo(aprsMap);
} }
selectedAprsTrackCall = String(marker._aprsCall); selectedAprsTrackCall = String(marker._aprsCall);
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path"); setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path");
return; return;
} }
@@ -4775,7 +4790,7 @@ function initAprsMap() {
ensureAisTrack(String(marker._aisMmsi), entry); ensureAisTrack(String(marker._aisMmsi), entry);
selectedAisTrackMmsi = String(marker._aisMmsi); selectedAisTrackMmsi = String(marker._aisMmsi);
syncSelectedAisTrackVisibility(); syncSelectedAisTrackVisibility();
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path"); setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("ais"), "aprs-radio-path");
return; return;
} }
@@ -4784,7 +4799,7 @@ function initAprsMap() {
const entry = vdesMarkers.get(String(marker._vdesKey)); const entry = vdesMarkers.get(String(marker._vdesKey));
if (!entry || !entry.msg) return; if (!entry || !entry.msg) return;
e.popup.setContent(buildVdesPopupHtml(entry.msg)); e.popup.setContent(buildVdesPopupHtml(entry.msg));
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path"); setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("vdes"), "aprs-radio-path");
return; return;
} }
@@ -4792,7 +4807,9 @@ function initAprsMap() {
const center = locatorMarkerCenter(marker); const center = locatorMarkerCenter(marker);
if (center) { if (center) {
setSelectedLocatorMarker(marker); setSelectedLocatorMarker(marker);
setMapRadioPathTo(center.lat, center.lon, "locator-radio-path"); const lEntry = locatorEntryForMarker(marker);
const lColor = lEntry ? locatorStyleForEntry(lEntry, locatorEntryCount(lEntry)).color : locatorFilterColor(marker.__trxType);
setMapRadioPathTo(center.lat, center.lon, lColor, "locator-radio-path");
} }
} }
}); });
@@ -5061,7 +5078,9 @@ window.navigateToMapLocator = function(grid, preferredType = null) {
if (center) { if (center) {
const targetZoom = Math.max(aprsMap.getZoom() || 0, 7); const targetZoom = Math.max(aprsMap.getZoom() || 0, 7);
aprsMap.setView([center.lat, center.lon], targetZoom); aprsMap.setView([center.lat, center.lon], targetZoom);
setMapRadioPathTo(center.lat, center.lon, "locator-radio-path"); const fEntry = locatorEntryForMarker(marker);
const fColor = fEntry ? locatorStyleForEntry(fEntry, locatorEntryCount(fEntry)).color : locatorFilterColor(marker?.__trxType);
setMapRadioPathTo(center.lat, center.lon, fColor, "locator-radio-path");
} }
setSelectedLocatorMarker(marker); setSelectedLocatorMarker(marker);
if (typeof marker.openPopup === "function") marker.openPopup(); if (typeof marker.openPopup === "function") marker.openPopup();
@@ -1816,13 +1816,11 @@ body.map-fake-fullscreen-active {
color: var(--text); color: var(--text);
word-break: break-word; word-break: break-word;
} }
.aprs-radio-path { stroke: var(--accent-green) !important; stroke-opacity: 0.8 !important; stroke-dasharray: 10 5 !important; animation: aprs-radio-path-flow 0.7s linear infinite; } .aprs-radio-path { stroke-dasharray: 10 5 !important; animation: aprs-radio-path-flow 0.7s linear infinite; }
.locator-radio-path { stroke: var(--accent-green) !important; stroke-opacity: 0.9 !important; stroke-dasharray: 12 6 !important; animation: aprs-radio-path-flow 0.7s linear infinite; } .locator-radio-path { stroke-dasharray: 12 6 !important; animation: aprs-radio-path-flow 0.7s linear infinite; }
.decode-contact-path { .decode-contact-path {
stroke: color-mix(in srgb, var(--accent-green) 72%, var(--accent-yellow)) !important;
stroke-opacity: 0.78 !important;
stroke-dasharray: 9 6 !important; stroke-dasharray: 9 6 !important;
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--accent-green) 34%, transparent)); filter: drop-shadow(0 0 3px color-mix(in srgb, currentColor 34%, transparent));
animation: aprs-radio-path-flow 0.85s linear infinite; animation: aprs-radio-path-flow 0.85s linear infinite;
} }
.decode-contact-distance-label { .decode-contact-distance-label {