[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 aprsMapBaseLayer = null;
|
||||||
let aprsMapReceiverMarker = null;
|
let aprsMapReceiverMarker = null;
|
||||||
let aprsRadioPath = null;
|
let aprsRadioPath = null;
|
||||||
|
let selectedLocatorMarker = null;
|
||||||
const stationMarkers = new Map();
|
const stationMarkers = new Map();
|
||||||
const locatorMarkers = new Map();
|
const locatorMarkers = new Map();
|
||||||
const mapMarkers = new Set();
|
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) {
|
function renderMapLocatorChipRow(container, items, selectedSet, kind) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
@@ -3489,6 +3554,10 @@ window.clearMapMarkersByType = function(type) {
|
|||||||
for (const [key, entry] of locatorMarkers.entries()) {
|
for (const [key, entry] of locatorMarkers.entries()) {
|
||||||
if (!key.startsWith(prefix)) continue;
|
if (!key.startsWith(prefix)) continue;
|
||||||
if (entry && entry.marker) {
|
if (entry && entry.marker) {
|
||||||
|
if (entry.marker === selectedLocatorMarker) {
|
||||||
|
setSelectedLocatorMarker(null);
|
||||||
|
clearMapRadioPath();
|
||||||
|
}
|
||||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.marker);
|
mapMarkers.delete(entry.marker);
|
||||||
}
|
}
|
||||||
@@ -3501,6 +3570,10 @@ window.clearMapMarkersByType = function(type) {
|
|||||||
for (const [key, entry] of locatorMarkers.entries()) {
|
for (const [key, entry] of locatorMarkers.entries()) {
|
||||||
if (!key.startsWith("bookmark:")) continue;
|
if (!key.startsWith("bookmark:")) continue;
|
||||||
if (entry && entry.marker) {
|
if (entry && entry.marker) {
|
||||||
|
if (entry.marker === selectedLocatorMarker) {
|
||||||
|
setSelectedLocatorMarker(null);
|
||||||
|
clearMapRadioPath();
|
||||||
|
}
|
||||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.marker);
|
mapMarkers.delete(entry.marker);
|
||||||
}
|
}
|
||||||
@@ -3557,7 +3630,8 @@ function initAprsMap() {
|
|||||||
// Rebuild popup content on open (keeps age/distance/rig list fresh)
|
// Rebuild popup content on open (keeps age/distance/rig list fresh)
|
||||||
aprsMap.on("popupopen", function(e) {
|
aprsMap.on("popupopen", function(e) {
|
||||||
const marker = e.popup._source;
|
const marker = e.popup._source;
|
||||||
if (aprsRadioPath) { aprsRadioPath.remove(); aprsRadioPath = null; }
|
clearMapRadioPath();
|
||||||
|
setSelectedLocatorMarker(null);
|
||||||
if (selectedAisTrackMmsi) {
|
if (selectedAisTrackMmsi) {
|
||||||
const prevEntry = aisMarkers.get(String(selectedAisTrackMmsi));
|
const prevEntry = aisMarkers.get(String(selectedAisTrackMmsi));
|
||||||
if (prevEntry && prevEntry.track && aprsMap && aprsMap.hasLayer(prevEntry.track)) {
|
if (prevEntry && prevEntry.track && aprsMap && aprsMap.hasLayer(prevEntry.track)) {
|
||||||
@@ -3572,23 +3646,19 @@ function initAprsMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!marker) return;
|
if (!marker) return;
|
||||||
|
const ll = typeof marker.getLatLng === "function" ? marker.getLatLng() : null;
|
||||||
const ll = marker.getLatLng();
|
|
||||||
|
|
||||||
if (marker._aprsCall) {
|
if (marker._aprsCall) {
|
||||||
|
if (!ll) return;
|
||||||
const entry = stationMarkers.get(marker._aprsCall);
|
const entry = stationMarkers.get(marker._aprsCall);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
|
||||||
if (serverLat != null && serverLon != null) {
|
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
|
||||||
aprsRadioPath = L.polyline(
|
|
||||||
[[serverLat, serverLon], [ll.lat, ll.lng]],
|
|
||||||
{ className: "aprs-radio-path", weight: 2, interactive: false }
|
|
||||||
).addTo(aprsMap);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker._aisMmsi) {
|
if (marker._aisMmsi) {
|
||||||
|
if (!ll) return;
|
||||||
const entry = aisMarkers.get(String(marker._aisMmsi));
|
const entry = aisMarkers.get(String(marker._aisMmsi));
|
||||||
if (!entry || !entry.msg) return;
|
if (!entry || !entry.msg) return;
|
||||||
e.popup.setContent(buildAisPopupHtml(entry.msg));
|
e.popup.setContent(buildAisPopupHtml(entry.msg));
|
||||||
@@ -3597,30 +3667,31 @@ function initAprsMap() {
|
|||||||
entry.track.addTo(aprsMap);
|
entry.track.addTo(aprsMap);
|
||||||
}
|
}
|
||||||
selectedAisTrackMmsi = String(marker._aisMmsi);
|
selectedAisTrackMmsi = String(marker._aisMmsi);
|
||||||
if (serverLat != null && serverLon != null) {
|
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
|
||||||
aprsRadioPath = L.polyline(
|
|
||||||
[[serverLat, serverLon], [ll.lat, ll.lng]],
|
|
||||||
{ className: "aprs-radio-path", weight: 2, interactive: false }
|
|
||||||
).addTo(aprsMap);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker._vdesKey) {
|
if (marker._vdesKey) {
|
||||||
|
if (!ll) return;
|
||||||
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));
|
||||||
if (serverLat != null && serverLon != null) {
|
setMapRadioPathTo(ll.lat, ll.lng, "aprs-radio-path");
|
||||||
aprsRadioPath = L.polyline(
|
return;
|
||||||
[[serverLat, serverLon], [ll.lat, ll.lng]],
|
}
|
||||||
{ className: "aprs-radio-path", weight: 2, interactive: false }
|
|
||||||
).addTo(aprsMap);
|
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() {
|
aprsMap.on("popupclose", function() {
|
||||||
if (aprsRadioPath) { aprsRadioPath.remove(); aprsRadioPath = null; }
|
clearMapRadioPath();
|
||||||
|
setSelectedLocatorMarker(null);
|
||||||
if (selectedAisTrackMmsi) {
|
if (selectedAisTrackMmsi) {
|
||||||
const entry = aisMarkers.get(String(selectedAisTrackMmsi));
|
const entry = aisMarkers.get(String(selectedAisTrackMmsi));
|
||||||
if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
|
if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
|
||||||
@@ -3646,6 +3717,7 @@ function initAprsMap() {
|
|||||||
.addTo(aprsMap)
|
.addTo(aprsMap)
|
||||||
.bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
|
.bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
|
||||||
entry.marker.__trxType = "bookmark";
|
entry.marker.__trxType = "bookmark";
|
||||||
|
sendLocatorOverlayToBack(entry.marker);
|
||||||
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
|
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
|
||||||
mapMarkers.add(entry.marker);
|
mapMarkers.add(entry.marker);
|
||||||
}
|
}
|
||||||
@@ -4235,7 +4307,10 @@ function applyMapFilter() {
|
|||||||
(type === "wspr" && mapFilter.wspr)
|
(type === "wspr" && mapFilter.wspr)
|
||||||
);
|
);
|
||||||
const onMap = aprsMap.hasLayer(marker);
|
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);
|
if (!visible && onMap) marker.removeFrom(aprsMap);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4349,6 +4424,10 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
|||||||
if (!key.startsWith("bookmark:")) continue;
|
if (!key.startsWith("bookmark:")) continue;
|
||||||
if (!grouped.has(key)) {
|
if (!grouped.has(key)) {
|
||||||
if (entry && entry.marker) {
|
if (entry && entry.marker) {
|
||||||
|
if (entry.marker === selectedLocatorMarker) {
|
||||||
|
setSelectedLocatorMarker(null);
|
||||||
|
clearMapRadioPath();
|
||||||
|
}
|
||||||
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
|
||||||
mapMarkers.delete(entry.marker);
|
mapMarkers.delete(entry.marker);
|
||||||
}
|
}
|
||||||
@@ -4370,6 +4449,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
|||||||
existing.marker.setBounds(next.bounds);
|
existing.marker.setBounds(next.bounds);
|
||||||
existing.marker.setStyle(locatorStyleForCount(next.bookmarks.length, "bookmark"));
|
existing.marker.setStyle(locatorStyleForCount(next.bookmarks.length, "bookmark"));
|
||||||
existing.marker.setPopupContent(popupHtml);
|
existing.marker.setPopupContent(popupHtml);
|
||||||
|
sendLocatorOverlayToBack(existing.marker);
|
||||||
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -4389,6 +4469,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
|
|||||||
.addTo(aprsMap)
|
.addTo(aprsMap)
|
||||||
.bindPopup(popupHtml);
|
.bindPopup(popupHtml);
|
||||||
entry.marker.__trxType = "bookmark";
|
entry.marker.__trxType = "bookmark";
|
||||||
|
sendLocatorOverlayToBack(entry.marker);
|
||||||
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
|
assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
|
||||||
mapMarkers.add(entry.marker);
|
mapMarkers.add(entry.marker);
|
||||||
}
|
}
|
||||||
@@ -4431,6 +4512,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
|||||||
const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType);
|
const tooltipHtml = buildDecodeLocatorTooltipHtml(grid, existing, markerType);
|
||||||
existing.marker.setStyle(locatorStyleForCount(count, markerType));
|
existing.marker.setStyle(locatorStyleForCount(count, markerType));
|
||||||
existing.marker.setPopupContent(tooltipHtml);
|
existing.marker.setPopupContent(tooltipHtml);
|
||||||
|
sendLocatorOverlayToBack(existing.marker);
|
||||||
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
|
||||||
rebuildMapLocatorFilters();
|
rebuildMapLocatorFilters();
|
||||||
applyMapFilter();
|
applyMapFilter();
|
||||||
@@ -4447,6 +4529,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
|||||||
.addTo(aprsMap)
|
.addTo(aprsMap)
|
||||||
.bindPopup(tooltipHtml);
|
.bindPopup(tooltipHtml);
|
||||||
marker.__trxType = markerType;
|
marker.__trxType = markerType;
|
||||||
|
sendLocatorOverlayToBack(marker);
|
||||||
const bandMeta = collectBandMeta(
|
const bandMeta = collectBandMeta(
|
||||||
Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz))
|
Array.from(stationDetails.values()).map((detail) => Number(detail?.freq_hz))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1514,9 +1514,15 @@ small { color: var(--text-muted); }
|
|||||||
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: 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; }
|
.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; }
|
.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 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 { 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-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; }
|
.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