fix: 移除Canvas像素缓存—每帧实时latLngToContainerPoint

根因: _trafficEdgeCache缓存旧像素位置,地图拖动/缩放后不更新
修复:
- 完全移除像素缓存,每帧重新计算latLngToContainerPoint
- Canvas为#saMap子元素,绝对定位,跟随容器
- 移除moveend缓存清除逻辑(不再需要)

效果: 地图平移/缩放时流量边自动跟随,不再是'贴片'

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TianXuan Developer
2026-07-25 10:56:23 +08:00
parent 6c7fbb28b1
commit 32586d3ed4
@@ -2577,9 +2577,7 @@ let _trafficPrevFrameTs = 0;
let _trafficSeekTarget = null;
// Canvas overlay
let _trafficCanvas = null;
let _trafficCtx = null;
let _trafficEdgeCache = {};
let _trafficCanvasId = 'saTrafficCanvas';
function toggleTrafficPanel() {
const panel = document.getElementById('saTrafficPanel');
@@ -2596,25 +2594,25 @@ function toggleTrafficPanel() {
function _ensureFlowCanvas() {
if (!SA.map) return null;
if (_trafficCanvas && _trafficCanvas.parentNode) return _trafficCanvas;
const mapEl = SA.map.getContainer();
const c = document.createElement('canvas');
c.id = 'saTrafficCanvasOverlay';
c.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10000;';
mapEl.appendChild(c);
let c = document.getElementById(_trafficCanvasId);
if (!c) {
c = document.createElement('canvas');
c.id = _trafficCanvasId;
c.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:10000;';
SA.map.getContainer().appendChild(c);
}
const s = SA.map.getSize();
c.width = s.x; c.height = s.y;
c.style.width = s.x + 'px'; c.style.height = s.y + 'px';
SA.map.on('resize', () => { const sz = SA.map.getSize(); c.width = sz.x; c.height = sz.y; c.style.width = sz.x + 'px'; c.style.height = sz.y + 'px'; });
_trafficCtx = c.getContext('2d');
_trafficCanvas = c;
if (c.width !== s.x || c.height !== s.y) {
c.width = s.x; c.height = s.y;
c.style.width = s.x + 'px'; c.style.height = s.y + 'px';
}
return c;
}
function _removeFlowCanvas() {
cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null;
if (_trafficCanvas) { _trafficCanvas.remove(); _trafficCanvas = null; _trafficCtx = null; }
_trafficEdgeCache = {};
const c = document.getElementById(_trafficCanvasId);
if (c) c.remove();
}
function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); }
@@ -2631,7 +2629,7 @@ function loadTrafficData() {
.then(d => {
hideLoading();
if (d.error) { showError(d.error); return; }
_trafficEdgeCache = {};
// Edge positions computed fresh each frame — no cache needed
let ws = d.window_seconds || 10;
let minTs = d.min_ts || (d.windows[0]?.ts || 0);
let maxTs = d.max_ts || (d.windows[d.n_windows-1]?.ts_end || minTs + ws);
@@ -2672,7 +2670,6 @@ function loadTrafficData() {
document.getElementById('saTrafficSlider').value = 0;
document.getElementById('saTrafficInfo').textContent =
(d.windows||[]).length + ' 窗口 · ' + Object.keys(d.nodes_coords||{}).length + ' 节点 · 跨度' + ((maxTs-minTs)/60).toFixed(0) + 'min';
SA.map.off('moveend.flowCache').on('moveend.flowCache', () => { _trafficEdgeCache = {}; if (_trafficPlaying) renderTrafficCrossfade(); });
document.getElementById('saTrafficTime').textContent = tsToBJ(_trafficCurrentTime);
trafficStart();
})
@@ -2772,7 +2769,7 @@ function trafficAnimate(timestamp) {
function renderTrafficCrossfade() {
const c = _ensureFlowCanvas();
if (!c || !SA.map || !_trafficData) return;
const ctx = _trafficCtx;
const ctx = c.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, c.width, c.height);
@@ -2809,39 +2806,34 @@ function renderTrafficCrossfade() {
edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime);
edges.forEach(e => {
let cached = _trafficEdgeCache[e.source + '|' + e.target];
if (!cached) {
const sc = coords[e.source], dc = coords[e.target];
if (!sc || !dc) { _trafficEdgeCache[e.source+'|'+e.target] = null; return; }
const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]);
const p2 = SA.map.latLngToContainerPoint([dc[0], dc[1]]);
if (!p1 || !p2) return;
cached = {p1x:p1.x, p1y:p1.y, p2x:p2.x, p2y:p2.y, midX:(p1.x+p2.x)/2, midY:(p1.y+p2.y)/2 - Math.abs(p2.x-p1.x)*0.1};
_trafficEdgeCache[e.source+'|'+e.target] = cached;
}
if (!cached) return;
const sc = coords[e.source], dc = coords[e.target];
if (!sc || !dc) return;
const p1 = SA.map.latLngToContainerPoint([sc[0], sc[1]]);
const p2 = SA.map.latLngToContainerPoint([dc[0], dc[1]]);
if (!p1 || !p2) return;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2 - Math.abs(p2.x - p1.x) * 0.1;
const ratio = Math.min(1, e.count / maxCount);
const alpha = 0.4 + 0.6 * e.weight;
// Edge trace with source→destination color gradient
const grad = ctx.createLinearGradient(cached.p1x, cached.p1y, cached.p2x, cached.p2y);
grad.addColorStop(0, 'rgba(46,196,182,' + alpha.toFixed(2) + ')'); // src: teal
grad.addColorStop(1, 'rgba(230,57,70,' + alpha.toFixed(2) + ')'); // dst: red
// Edge trace with src→dst color gradient
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
grad.addColorStop(0, 'rgba(46,196,182,' + alpha.toFixed(2) + ')');
grad.addColorStop(1, 'rgba(230,57,70,' + alpha.toFixed(2) + ')');
ctx.beginPath();
ctx.moveTo(cached.p1x, cached.p1y);
ctx.quadraticCurveTo(cached.midX, cached.midY, cached.p2x, cached.p2y);
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(midX, midY, p2.x, p2.y);
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5 + 4 * ratio;
ctx.stroke();
// Particles: source(teal)→destination(red), speed ∝ traffic volume
const speedMul = 0.5 + 1.5 * ratio; // higher traffic → faster particles
// Flowing particles
const speedMul = 0.5 + 1.5 * ratio;
const nP = Math.ceil(3 + 6 * ratio);
for (let pi = 0; pi < nP; pi++) {
const pt = ((Date.now() / 2000 * speedMul + pi / nP) % 1);
const px = (1-pt)*(1-pt)*cached.p1x + 2*(1-pt)*pt*cached.midX + pt*pt*cached.p2x;
const py = (1-pt)*(1-pt)*cached.p1y + 2*(1-pt)*pt*cached.midY + pt*pt*cached.p2y;
// src teal→dst red gradient
const px = (1-pt)*(1-pt)*p1.x + 2*(1-pt)*pt*midX + pt*pt*p2.x;
const py = (1-pt)*(1-pt)*p1.y + 2*(1-pt)*pt*midY + pt*pt*p2.y;
const cr = Math.round(46 + 184*pt), cg = Math.round(196 - 139*pt), cb = Math.round(182 - 112*pt);
ctx.beginPath();
ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2);