[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;
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() {
@@ -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) {
if (!entry || !aprsMap) return;
const linePoints = [
[entry.from.lat, entry.from.lon],
[entry.to.lat, entry.to.lon],
];
const color = decodeContactPathColor(entry);
if (!entry.line) {
entry.line = L.polyline(linePoints, {
color,
opacity: 0.78,
className: "decode-contact-path",
weight: 2.8,
interactive: false,
}).addTo(aprsMap);
} else {
entry.line.setLatLngs(linePoints);
entry.line.setStyle({ color, opacity: 0.78 });
if (!aprsMap.hasLayer(entry.line)) entry.line.addTo(aprsMap);
}
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();
if (!mapP2pRadioPathsEnabled || serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
return;
}
aprsRadioPath = L.polyline(
[[serverLat, serverLon], [lat, lon]],
{ className, weight: 2, interactive: false }
{ color, opacity: 0.85, weight: 2, interactive: false, className }
).addTo(aprsMap);
}
@@ -4763,7 +4778,7 @@ function initAprsMap() {
entry.track.addTo(aprsMap);
}
selectedAprsTrackCall = String(marker._aprsCall);
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path");
return;
}
@@ -4775,7 +4790,7 @@ function initAprsMap() {
ensureAisTrack(String(marker._aisMmsi), entry);
selectedAisTrackMmsi = String(marker._aisMmsi);
syncSelectedAisTrackVisibility();
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("ais"), "aprs-radio-path");
return;
}
@@ -4784,7 +4799,7 @@ function initAprsMap() {
const entry = vdesMarkers.get(String(marker._vdesKey));
if (!entry || !entry.msg) return;
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;
}
@@ -4792,7 +4807,9 @@ function initAprsMap() {
const center = locatorMarkerCenter(marker);
if (center) {
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) {
const targetZoom = Math.max(aprsMap.getZoom() || 0, 7);
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);
if (typeof marker.openPopup === "function") marker.openPopup();
@@ -1816,13 +1816,11 @@ body.map-fake-fullscreen-active {
color: var(--text);
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; }
.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; }
.aprs-radio-path { stroke-dasharray: 10 5 !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 {
stroke: color-mix(in srgb, var(--accent-green) 72%, var(--accent-yellow)) !important;
stroke-opacity: 0.78 !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;
}
.decode-contact-distance-label {