[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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user