[fix](trx-frontend): animate selected locator paths

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-04 19:02:31 +01:00
parent d1968bd2ba
commit 283125989b
2 changed files with 111 additions and 22 deletions
@@ -3170,6 +3170,7 @@ let aprsMap = null;
let aprsMapBaseLayer = null;
let aprsMapReceiverMarker = null;
let aprsRadioPath = null;
let selectedLocatorMarker = null;
const stationMarkers = new Map();
const locatorMarkers = new Map();
const mapMarkers = new Set();
@@ -3278,6 +3279,70 @@ function assignLocatorMarkerMeta(marker, sourceType, bandMeta) {
};
}
function clearMapRadioPath() {
if (aprsRadioPath) {
aprsRadioPath.remove();
aprsRadioPath = null;
}
}
function setMapRadioPathTo(lat, lon, className = "aprs-radio-path") {
clearMapRadioPath();
if (serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
return;
}
aprsRadioPath = L.polyline(
[[serverLat, serverLon], [lat, lon]],
{ className, weight: 2, interactive: false }
).addTo(aprsMap);
}
function locatorMarkerCenter(marker) {
if (!marker) return null;
if (typeof marker.getBounds === "function") {
const bounds = marker.getBounds();
if (bounds && typeof bounds.getCenter === "function") {
const center = bounds.getCenter();
if (Number.isFinite(center?.lat) && Number.isFinite(center?.lng)) {
return { lat: center.lat, lon: center.lng };
}
}
}
if (typeof marker.getLatLng === "function") {
const ll = marker.getLatLng();
if (Number.isFinite(ll?.lat) && Number.isFinite(ll?.lng)) {
return { lat: ll.lat, lon: ll.lng };
}
}
return null;
}
function setLocatorMarkerHighlight(marker, enabled) {
const element = typeof marker?.getElement === "function" ? marker.getElement() : marker?._path;
if (!element) return;
element.classList.toggle("trx-locator-selected", !!enabled);
}
function setSelectedLocatorMarker(marker) {
if (selectedLocatorMarker && selectedLocatorMarker !== marker) {
setLocatorMarkerHighlight(selectedLocatorMarker, false);
}
selectedLocatorMarker = marker || null;
if (selectedLocatorMarker) {
setLocatorMarkerHighlight(selectedLocatorMarker, true);
}
}
function isLocatorOverlay(marker) {
const type = marker?.__trxType;
return type === "bookmark" || type === "ft8" || type === "wspr";
}
function sendLocatorOverlayToBack(marker) {
if (!isLocatorOverlay(marker) || typeof marker?.bringToBack !== "function") return;
marker.bringToBack();
}
function renderMapLocatorChipRow(container, items, selectedSet, kind) {
if (!container) return;
container.innerHTML = "";
@@ -3489,6 +3554,10 @@ window.clearMapMarkersByType = function(type) {
for (const [key, entry] of locatorMarkers.entries()) {
if (!key.startsWith(prefix)) continue;
if (entry && entry.marker) {
if (entry.marker === selectedLocatorMarker) {
setSelectedLocatorMarker(null);
clearMapRadioPath();
}
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
mapMarkers.delete(entry.marker);
}
@@ -3501,6 +3570,10 @@ window.clearMapMarkersByType = function(type) {
for (const [key, entry] of locatorMarkers.entries()) {
if (!key.startsWith("bookmark:")) continue;
if (entry && entry.marker) {
if (entry.marker === selectedLocatorMarker) {
setSelectedLocatorMarker(null);
clearMapRadioPath();
}
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
mapMarkers.delete(entry.marker);
}
@@ -3557,7 +3630,8 @@ function initAprsMap() {
// Rebuild popup content on open (keeps age/distance/rig list fresh)
aprsMap.on("popupopen", function(e) {
const marker = e.popup._source;
if (aprsRadioPath) { aprsRadioPath.remove(); aprsRadioPath = null; }
clearMapRadioPath();
setSelectedLocatorMarker(null);
if (selectedAisTrackMmsi) {
const prevEntry = aisMarkers.get(String(selectedAisTrackMmsi));
if (prevEntry && prevEntry.track && aprsMap && aprsMap.hasLayer(prevEntry.track)) {
@@ -3572,23 +3646,19 @@ function initAprsMap() {
}
if (!marker) return;
const ll = marker.getLatLng();
const ll = typeof marker.getLatLng === "function" ? marker.getLatLng() : null;
if (marker._aprsCall) {
if (!ll) return;
const entry = stationMarkers.get(marker._aprsCall);
if (!entry) return;
e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
if (serverLat != null && serverLon != null) {
aprsRadioPath = L.polyline(
[[serverLat, serverLon], [ll.lat, ll.lng]],
{ className: "aprs-radio-path", weight: 2, interactive: false }
).addTo(aprsMap);
}
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
return;
}
if (marker._aisMmsi) {
if (!ll) return;
const entry = aisMarkers.get(String(marker._aisMmsi));
if (!entry || !entry.msg) return;
e.popup.setContent(buildAisPopupHtml(entry.msg));
@@ -3597,30 +3667,31 @@ function initAprsMap() {
entry.track.addTo(aprsMap);
}
selectedAisTrackMmsi = String(marker._aisMmsi);
if (serverLat != null && serverLon != null) {
aprsRadioPath = L.polyline(
[[serverLat, serverLon], [ll.lat, ll.lng]],
{ className: "aprs-radio-path", weight: 2, interactive: false }
).addTo(aprsMap);
}
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
return;
}
if (marker._vdesKey) {
if (!ll) return;
const entry = vdesMarkers.get(String(marker._vdesKey));
if (!entry || !entry.msg) return;
e.popup.setContent(buildVdesPopupHtml(entry.msg));
if (serverLat != null && serverLon != null) {
aprsRadioPath = L.polyline(
[[serverLat, serverLon], [ll.lat, ll.lng]],
{ className: "aprs-radio-path", weight: 2, interactive: false }
).addTo(aprsMap);
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
return;
}
if (marker.__trxType === "bookmark" || marker.__trxType === "ft8" || marker.__trxType === "wspr") {
const center = locatorMarkerCenter(marker);
if (center) {
setSelectedLocatorMarker(marker);
setMapRadioPathTo(center.lat, center.lon, "locator-radio-path");
}
}
});
aprsMap.on("popupclose", function() {
if (aprsRadioPath) { aprsRadioPath.remove(); aprsRadioPath = null; }
clearMapRadioPath();
setSelectedLocatorMarker(null);
if (selectedAisTrackMmsi) {
const entry = aisMarkers.get(String(selectedAisTrackMmsi));
if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
@@ -3646,6 +3717,7 @@ function initAprsMap() {
.addTo(aprsMap)
.bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
entry.marker.__trxType = "bookmark";
sendLocatorOverlayToBack(entry.marker);
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
mapMarkers.add(entry.marker);
}
@@ -4235,7 +4307,10 @@ function applyMapFilter() {
(type === "wspr" && mapFilter.wspr)
);
const onMap = aprsMap.hasLayer(marker);
if (visible && !onMap) marker.addTo(aprsMap);
if (visible && !onMap) {
marker.addTo(aprsMap);
sendLocatorOverlayToBack(marker);
}
if (!visible && onMap) marker.removeFrom(aprsMap);
});
}
@@ -4349,6 +4424,10 @@ window.syncBookmarkMapLocators = function(bookmarks) {
if (!key.startsWith("bookmark:")) continue;
if (!grouped.has(key)) {
if (entry && entry.marker) {
if (entry.marker === selectedLocatorMarker) {
setSelectedLocatorMarker(null);
clearMapRadioPath();
}
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
mapMarkers.delete(entry.marker);
}
@@ -4370,6 +4449,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
existing.marker.setBounds(next.bounds);
existing.marker.setStyle(locatorStyleForCount(next.bookmarks.length, "bookmark"));
existing.marker.setPopupContent(popupHtml);
sendLocatorOverlayToBack(existing.marker);
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
}
continue;
@@ -4389,6 +4469,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
.addTo(aprsMap)
.bindPopup(popupHtml);
entry.marker.__trxType = "bookmark";
sendLocatorOverlayToBack(entry.marker);
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
mapMarkers.add(entry.marker);
}
@@ -4431,6 +4512,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType);
existing.marker.setStyle(locatorStyleForCount(count, markerType));
existing.marker.setPopupContent(tooltipHtml);
sendLocatorOverlayToBack(existing.marker);
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
rebuildMapLocatorFilters();
applyMapFilter();
@@ -4447,6 +4529,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
.addTo(aprsMap)
.bindPopup(tooltipHtml);
marker.__trxType = markerType;
sendLocatorOverlayToBack(marker);
const bandMeta = collectBandMeta(
Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz))
);
@@ -1514,9 +1514,15 @@ small { color: var(--text-muted); }
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: #6ee7b7 !important; stroke-opacity: 0.9 !important; stroke-dasharray: 12 6 !important; animation: aprs-radio-path-flow 0.7s linear infinite; }
.trx-locator-selected { stroke-opacity: 1 !important; stroke-width: 3.25px !important; filter: drop-shadow(0 0 6px rgba(110, 231, 183, 0.5)); animation: trx-locator-breathe 1.6s ease-in-out infinite; }
.trx-receiver-marker { stroke: var(--accent-green) !important; fill: var(--accent-green) !important; }
.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; } }
@keyframes trx-locator-breathe {
0%, 100% { stroke-width: 2.4px; stroke-opacity: 0.78; filter: drop-shadow(0 0 2px rgba(110, 231, 183, 0.18)); }
50% { stroke-width: 4.2px; stroke-opacity: 1; filter: drop-shadow(0 0 10px rgba(110, 231, 183, 0.52)); }
}
.aprs-bar-pos { background: none; border: none; padding: 0; margin-left: 0.4em; font-family: inherit; font-size: inherit; color: var(--accent-green); cursor: pointer; }
.aprs-bar-pos:hover { text-decoration: underline; }
.aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; }