diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html index ac1eb34..5ad1355 100644 --- a/simple_analysis/templates/simple_analysis/simple_analysis.html +++ b/simple_analysis/templates/simple_analysis/simple_analysis.html @@ -2577,10 +2577,9 @@ let _trafficPrevFrameTs = 0; let _trafficSeekTarget = null; // Canvas overlay -let _trafficFlowLayer = null; -let _trafficDashOffset = 0; -let _trafficDashId = null; -let _trafficActiveEdges = {}; +let _trafficCanvas = null; +let _trafficCtx = null; +let _trafficEdgeCache = {}; function toggleTrafficPanel() { const panel = document.getElementById('saTrafficPanel'); @@ -2588,27 +2587,34 @@ function toggleTrafficPanel() { if (panel.classList.contains('open')) { trafficStop(); panel.classList.remove('open'); - _removeFlowLayer(); + _removeFlowCanvas(); } else { panel.classList.add('open'); loadTrafficData(); } } -function _ensureFlowLayer() { +function _ensureFlowCanvas() { if (!SA.map) return null; - if (!_trafficFlowLayer) { - const svgRenderer = L.svg({padding: 0.5}); - _trafficFlowLayer = L.layerGroup([], {renderer: svgRenderer}).addTo(SA.map); - } - return _trafficFlowLayer; + 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); + 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; + return c; } -function _removeFlowLayer() { +function _removeFlowCanvas() { cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null; - cancelAnimationFrame(_trafficDashId); _trafficDashId = null; - if (_trafficFlowLayer) { SA.map.removeLayer(_trafficFlowLayer); _trafficFlowLayer = null; } - _trafficActiveEdges = {}; + if (_trafficCanvas) { _trafficCanvas.remove(); _trafficCanvas = null; _trafficCtx = null; } + _trafficEdgeCache = {}; } function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); } @@ -2724,7 +2730,7 @@ function trafficPause() { } function trafficStop() { trafficPause(); _trafficData = null; - _removeFlowLayer(); + _removeFlowCanvas(); document.getElementById('saTrafficTime').textContent = '--:--:--'; document.getElementById('saTrafficInfo').textContent = ''; if (SA.pointLayer) SA.pointLayer.getLayers().forEach(l => { @@ -2763,8 +2769,11 @@ function trafficAnimate(timestamp) { } function renderTrafficCrossfade() { - const layer = _ensureFlowLayer(); - if (!layer || !SA.map || !_trafficData) return; + const c = _ensureFlowCanvas(); + if (!c || !SA.map || !_trafficData) return; + const ctx = _trafficCtx; + if (!ctx) return; + ctx.clearRect(0, 0, c.width, c.height); const wins = _trafficData.windows || []; if (!wins.length) return; @@ -2774,7 +2783,6 @@ function renderTrafficCrossfade() { const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null; const t = mix.t; - // Build edge map with crossfade weights const edgeMap = {}; function add(edges, w) { if (!edges) return; @@ -2789,7 +2797,9 @@ function renderTrafficCrossfade() { add(w1?w1.edges:[], t); const edges = Object.values(edgeMap); + if (!edges.length) return; const maxCount = Math.max(1, ...edges.map(e => e.count)); + const activeNodes = new Set(); if (w0 && 1-t > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n)); if (w1 && t > 0.1) (w1.active_nodes||[]).forEach(n => activeNodes.add(n)); @@ -2797,61 +2807,48 @@ function renderTrafficCrossfade() { document.getElementById('saTrafficInfo').textContent = edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime); - // Build/track Leaflet polylines with dash animation - const newKeys = new Set(); edges.forEach(e => { - const k = e.source + '|' + e.target; - newKeys.add(k); + 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 ratio = Math.min(1, e.count / maxCount); const alpha = 0.4 + 0.6 * e.weight; - const w = 1.5 + 4 * ratio; - const srcC = coords[e.source], dstC = coords[e.target]; - if (!srcC || !dstC) return; - let entry = _trafficActiveEdges[k]; - if (entry && entry.poly) { - // Update existing polyline style - entry.poly.setStyle({ - opacity: alpha, weight: w, - dashOffset: String(-_trafficDashOffset), - }); - entry.lastWindow = mix.i; - } else { - // Create new polyline - const poly = L.polyline([[srcC[0],srcC[1]], [dstC[0],dstC[1]]], { - color: '#4361ee', weight: w, opacity: alpha, - dashArray: '15 25', - dashOffset: String(-_trafficDashOffset), - interactive: false, bubblingMouseEvents: false, - }); - poly.addTo(layer); - _trafficActiveEdges[k] = {poly, lastWindow: mix.i}; + // Bezier curve + ctx.beginPath(); + ctx.moveTo(cached.p1x, cached.p1y); + ctx.quadraticCurveTo(cached.midX, cached.midY, cached.p2x, cached.p2y); + ctx.strokeStyle = 'rgba(67,97,238,' + alpha.toFixed(2) + ')'; + ctx.lineWidth = 1.5 + 4 * ratio; + ctx.stroke(); + + // Particles flowing along curve (Date.now() driven, continuous) + const nP = Math.ceil(3 + 5 * ratio); + for (let pi = 0; pi < nP; pi++) { + const pt = ((Date.now() / 2000 + 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; + const r = Math.round(67 + 163*pt), g = Math.round(97 - 40*pt), b = Math.round(238 - 168*pt); + ctx.beginPath(); + ctx.arc(px, py, 2 + 3*ratio, 0, Math.PI*2); + ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + (0.5+0.5*e.weight) + ')'; + ctx.shadowColor = 'rgba(' + r + ',' + g + ',' + b + ',0.5)'; + ctx.shadowBlur = 4; + ctx.fill(); + ctx.shadowBlur = 0; } }); - // Remove polylines no longer active - Object.keys(_trafficActiveEdges).forEach(k => { - if (!newKeys.has(k)) { - layer.removeLayer(_trafficActiveEdges[k].poly); - delete _trafficActiveEdges[k]; - } - }); + SA.map.off('moveend.flowCache').on('moveend.flowCache', () => { _trafficEdgeCache = {}; }); - // Animate dash offset for flowing effect - if (!_trafficDashId) { - function animDash() { - _trafficDashOffset = (_trafficDashOffset + 0.3) % 40; - if (_trafficFlowLayer) { - _trafficFlowLayer.getLayers().forEach(p => { - if (p.setStyle) p.setStyle({dashOffset: String(-_trafficDashOffset)}); - }); - } - _trafficDashId = requestAnimationFrame(animDash); - } - _trafficDashId = requestAnimationFrame(animDash); - } - - // Node dimming if (SA.pointLayer && !SA._clusterFocus) { SA.pointLayer.getLayers().forEach(l => { if (!l.setStyle) return;