[fix](trx-frontend-http): AIS map markers and tracks follow theme accent color
Read --accent-red CSS variable at draw time so markers, track lines, and TrackSymbol icons automatically match the active color scheme. Add refreshAisMarkerColors() called on theme toggle and style picker changes to repaint existing markers without a page reload. Also buffer live SSE decode messages until the /decode/history fetch settles to eliminate the history-appears-after-reload race condition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -664,6 +664,7 @@ if (themeToggleBtn) {
|
|||||||
setTheme(currentTheme() === "dark" ? "light" : "dark");
|
setTheme(currentTheme() === "dark" ? "light" : "dark");
|
||||||
updateMapBaseLayerForTheme(currentTheme());
|
updateMapBaseLayerForTheme(currentTheme());
|
||||||
syncLocatorMarkerStyles();
|
syncLocatorMarkerStyles();
|
||||||
|
refreshAisMarkerColors();
|
||||||
scheduleOverviewDraw();
|
scheduleOverviewDraw();
|
||||||
if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw();
|
if (typeof scheduleSpectrumDraw === "function" && lastSpectrumData) scheduleSpectrumDraw();
|
||||||
});
|
});
|
||||||
@@ -674,6 +675,7 @@ if (headerStylePickSelect) {
|
|||||||
setStyle(headerStylePickSelect.value);
|
setStyle(headerStylePickSelect.value);
|
||||||
updateMapBaseLayerForTheme(currentTheme());
|
updateMapBaseLayerForTheme(currentTheme());
|
||||||
syncLocatorMarkerStyles();
|
syncLocatorMarkerStyles();
|
||||||
|
refreshAisMarkerColors();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4981,7 +4983,7 @@ function ensureAisTrack(mmsi, entry) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const track = L.polyline(entry.trackPoints, {
|
const track = L.polyline(entry.trackPoints, {
|
||||||
color: "#ff7559",
|
color: getAisAccentColor(),
|
||||||
weight: 2,
|
weight: 2,
|
||||||
opacity: 0.68,
|
opacity: 0.68,
|
||||||
lineCap: "round",
|
lineCap: "round",
|
||||||
@@ -5011,13 +5013,18 @@ function syncSelectedAisTrackVisibility() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAisAccentColor() {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue("--accent-red").trim() || "#ff7559";
|
||||||
|
}
|
||||||
|
|
||||||
function aisMarkerOptionsFromMessage(msg) {
|
function aisMarkerOptionsFromMessage(msg) {
|
||||||
|
const color = getAisAccentColor();
|
||||||
return {
|
return {
|
||||||
heading: msg?.heading_deg,
|
heading: msg?.heading_deg,
|
||||||
course: msg?.cog_deg,
|
course: msg?.cog_deg,
|
||||||
speed: msg?.sog_knots,
|
speed: msg?.sog_knots,
|
||||||
color: "#ff7559",
|
color,
|
||||||
outline: "#6b2118",
|
outline: "#00000055",
|
||||||
size: 22,
|
size: 22,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5026,10 +5033,11 @@ function createAisMarker(lat, lon, msg) {
|
|||||||
if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") {
|
if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") {
|
||||||
return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg));
|
return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg));
|
||||||
}
|
}
|
||||||
|
const color = getAisAccentColor();
|
||||||
return L.circleMarker([lat, lon], {
|
return L.circleMarker([lat, lon], {
|
||||||
radius: 6,
|
radius: 6,
|
||||||
color: "#e2553d",
|
color,
|
||||||
fillColor: "#ff7559",
|
fillColor: color,
|
||||||
fillOpacity: 0.82,
|
fillOpacity: 0.82,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5041,17 +5049,33 @@ function updateAisMarker(marker, msg, popupHtml) {
|
|||||||
marker.setAisState(aisMarkerOptionsFromMessage(msg));
|
marker.setAisState(aisMarkerOptionsFromMessage(msg));
|
||||||
}
|
}
|
||||||
if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") {
|
if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") {
|
||||||
const hasHeading = Number.isFinite(msg?.heading_deg) || Number.isFinite(msg?.cog_deg);
|
const color = getAisAccentColor();
|
||||||
marker.setStyle({
|
marker.setStyle({
|
||||||
radius: hasHeading ? 6.5 : 6,
|
radius: 6,
|
||||||
color: hasHeading ? "#c8412f" : "#e2553d",
|
color,
|
||||||
fillColor: hasHeading ? "#ff6f4d" : "#ff7559",
|
fillColor: color,
|
||||||
fillOpacity: 0.84,
|
fillOpacity: 0.84,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
marker.setPopupContent(popupHtml);
|
marker.setPopupContent(popupHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshAisMarkerColors() {
|
||||||
|
const color = getAisAccentColor();
|
||||||
|
aisMarkers.forEach((entry) => {
|
||||||
|
if (entry.marker) {
|
||||||
|
if (typeof entry.marker.setAisState === "function") {
|
||||||
|
entry.marker.setAisState(aisMarkerOptionsFromMessage(entry.msg || {}));
|
||||||
|
} else if (typeof entry.marker.setStyle === "function") {
|
||||||
|
entry.marker.setStyle({ color, fillColor: color });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.track && typeof entry.track.setStyle === "function") {
|
||||||
|
entry.track.setStyle({ color });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.aisMapAddVessel = function(msg) {
|
window.aisMapAddVessel = function(msg) {
|
||||||
if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return;
|
if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return;
|
||||||
if (!aprsMap) initAprsMap();
|
if (!aprsMap) initAprsMap();
|
||||||
@@ -6154,9 +6178,20 @@ function connectDecode() {
|
|||||||
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
if (window.resetFt8HistoryView) window.resetFt8HistoryView();
|
||||||
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
|
if (window.resetWsprHistoryView) window.resetWsprHistoryView();
|
||||||
|
|
||||||
// Open the live SSE stream first so real-time messages are never blocked by
|
// Buffer live messages until history fetch settles so history always appears
|
||||||
// history replay. History is fetched separately via a plain HTTP request and
|
// before any live updates, regardless of network ordering.
|
||||||
// drained in the background using the existing chunked helper.
|
let historySettled = false;
|
||||||
|
const liveBuffer = [];
|
||||||
|
function flushLiveBuffer() {
|
||||||
|
historySettled = true;
|
||||||
|
for (const msg of liveBuffer) {
|
||||||
|
try { dispatchDecodeMessage(msg); } catch (_) {}
|
||||||
|
}
|
||||||
|
liveBuffer.length = 0;
|
||||||
|
}
|
||||||
|
// Safety valve: if the history fetch hangs, unblock after 8 s.
|
||||||
|
const historyTimeout = setTimeout(() => { if (!historySettled) flushLiveBuffer(); }, 8000);
|
||||||
|
|
||||||
decodeSource = new EventSource("/decode");
|
decodeSource = new EventSource("/decode");
|
||||||
decodeSource.onopen = () => {
|
decodeSource.onopen = () => {
|
||||||
decodeConnected = true;
|
decodeConnected = true;
|
||||||
@@ -6164,7 +6199,9 @@ function connectDecode() {
|
|||||||
};
|
};
|
||||||
decodeSource.onmessage = (evt) => {
|
decodeSource.onmessage = (evt) => {
|
||||||
try {
|
try {
|
||||||
dispatchDecodeMessage(JSON.parse(evt.data));
|
const msg = JSON.parse(evt.data);
|
||||||
|
if (historySettled) dispatchDecodeMessage(msg);
|
||||||
|
else liveBuffer.push(msg);
|
||||||
} catch (e) { /* ignore parse errors */ }
|
} catch (e) { /* ignore parse errors */ }
|
||||||
};
|
};
|
||||||
decodeSource.onerror = () => {
|
decodeSource.onerror = () => {
|
||||||
@@ -6181,13 +6218,15 @@ function connectDecode() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch history in parallel — does not block the live SSE stream.
|
// Fetch history in parallel; drain it first, then flush buffered live msgs.
|
||||||
fetch("/decode/history").then((resp) => {
|
fetch("/decode/history").then((resp) => {
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return null;
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}).then((msgs) => {
|
}).then((msgs) => {
|
||||||
|
clearTimeout(historyTimeout);
|
||||||
if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0);
|
if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0);
|
||||||
}).catch(() => { /* history unavailable, ignore */ });
|
flushLiveBuffer();
|
||||||
|
}).catch(() => { clearTimeout(historyTimeout); flushLiveBuffer(); });
|
||||||
}
|
}
|
||||||
if (document.readyState === "complete") {
|
if (document.readyState === "complete") {
|
||||||
connectDecode();
|
connectDecode();
|
||||||
|
|||||||
Reference in New Issue
Block a user