diff --git a/public/live.css b/public/live.css
index 6f836673..06b66fe9 100644
--- a/public/live.css
+++ b/public/live.css
@@ -1287,3 +1287,10 @@
.lc-path-chain { margin-top: 4px; word-break: break-word; }
.lc-path-link-wrap { margin-top: 4px; }
.lc-path-link { font-size: 11px; }
+
+/* Eliminate SVG baseline drift inside Leaflet divIcons */
+.live-node-marker {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/public/live.js b/public/live.js
index 6521336c..794ec7f2 100644
--- a/public/live.js
+++ b/public/live.js
@@ -1226,8 +1226,8 @@
// Add a 20% buffer around the visible screen to prevent clipping during short pans
const size = map.getSize();
- const padX = size.x * 0.2;
- const padY = size.y * 0.2;
+ const padX = Math.round(size.x * 0.2);
+ const padY = Math.round(size.y * 0.2);
const w = size.x + padX * 2;
const h = size.y + padY * 2;
@@ -1242,9 +1242,13 @@
animCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
- // Find the top-left Layer Point (relative to the pane, not the screen)
- const centerPt = map.latLngToLayerPoint(map.getCenter());
- canvasTopLeft = L.point(centerPt.x - w / 2, centerPt.y - h / 2);
+ // Find the absolute pixel bounds of the current viewport
+ const pixelBounds = map.getPixelBounds();
+ // Pad the bounds by 20% on all sides
+ const min = pixelBounds.min.subtract([padX, padY]);
+
+ // Convert absolute pixel coordinates to Layer points (relative to the pane)
+ canvasTopLeft = min.subtract(map.getPixelOrigin());
// Let Leaflet position the canvas inside the pane using CSS transforms
L.DomUtil.setPosition(animCanvas, canvasTopLeft);
@@ -2202,8 +2206,8 @@
| Last Seen | ${lastSeen} |
| Adverts | ${n.advert_count || 0} |
${'default_scope' in n ? `| Scope | ${n.default_scope === null ? '—'
- : n.default_scope === '' ? 'unknown scope'
- : `${escapeHtml(n.default_scope)}`
+ : n.default_scope === '' ? 'unknown scope'
+ : `${escapeHtml(n.default_scope)}`
} |
` : ''}
${hasLoc ? `| Location | ${n.lat.toFixed(5)}, ${n.lon.toFixed(5)} |
` : ''}
${stats.avgSnr != null ? `| Avg SNR | ${stats.avgSnr.toFixed(1)} dB |
` : ''}
@@ -2527,7 +2531,11 @@
// Shape-aware sizing: keep prior visual weight (~6/4 base) but
// route through divIcon so colourblind ops get distinct silhouettes
// (#1293). Size is the SVG box; circleMarker radius ~= size/3.
- const sizePx = Math.max(10, Math.round((isRepeater ? 18 : 14) * zoomScale));
+ let sizePx = Math.max(10, Math.round((isRepeater ? 18 : 14) * zoomScale));
+ // Force sizePx to be an even number so iconAnchor (sizePx/2) is always an integer.
+ // This prevents browser sub-pixel snapping of the divIcon, which causes
+ // it to visibly misalign with mathematically-exact Leaflet SVG paths underneath.
+ if (sizePx % 2 !== 0) sizePx += 1;
const svgHtml = (window.makeRoleMarkerSVG
? window.makeRoleMarkerSVG(n.role, null, sizePx)
@@ -2601,6 +2609,14 @@
function _liveSetMarkerSize(marker, sizePx) {
var el = _liveMarkerEl(marker);
if (!el) return;
+
+ // Update the DOM container styles manually so the anchor remains centered
+ // without having to destroy and recreate the Leaflet marker object.
+ el.style.width = sizePx + 'px';
+ el.style.height = sizePx + 'px';
+ el.style.marginLeft = -(sizePx / 2) + 'px';
+ el.style.marginTop = -(sizePx / 2) + 'px';
+
var svg = el.querySelector('svg');
if (svg) {
svg.setAttribute('width', sizePx);
@@ -2631,7 +2647,9 @@
for (const [key, marker] of Object.entries(nodeMarkers)) {
const n = nodeData[key];
const isRepeater = n && n.role === 'repeater';
- const sizePx = Math.max(10, Math.round((isRepeater ? 18 : 14) * zoomScale));
+ let sizePx = Math.max(10, Math.round((isRepeater ? 18 : 14) * zoomScale));
+ // Force sizePx to be even to prevent sub-pixel anchor drift
+ if (sizePx % 2 !== 0) sizePx += 1;
_liveSetMarkerSize(marker, sizePx);
}
}
@@ -3535,8 +3553,8 @@
const W = animCanvas.clientWidth;
const H = animCanvas.clientHeight;
- const cull = (fromPt.x < 0 && toPt.x < 0) || (fromPt.x > W && toPt.x > W) ||
- (fromPt.y < 0 && toPt.y < 0) || (fromPt.y > H && toPt.y > H);
+ const cull = (fromPt.x < 0 && toPt.x < 0) || (fromPt.x > W && toPt.x > W) ||
+ (fromPt.y < 0 && toPt.y < 0) || (fromPt.y > H && toPt.y > H);
if (!cull) {
const currentX = fromPt.x + (toPt.x - fromPt.x) * t;
@@ -4030,7 +4048,7 @@
// test-live-mql-leak-1180-e2e.js and otherwise unused.
var _liveNarrowMqlBound = false;
window._liveTestSeams = window._liveTestSeams || {};
- window._liveTestSeams.wake = function() {
+ window._liveTestSeams.wake = function () {
if (!isAnimating) {
isAnimating = true;
requestAnimationFrame(renderAnimations);