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);