diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html index 603c685..fbfef37 100644 --- a/simple_analysis/templates/simple_analysis/simple_analysis.html +++ b/simple_analysis/templates/simple_analysis/simple_analysis.html @@ -2577,11 +2577,10 @@ let _trafficPrevFrameTs = 0; let _trafficSeekTarget = null; // Canvas overlay -let _trafficCanvas = null; -let _trafficCtx = null; - -// Cached pixel positions for edges (rebuilt on map move) -let _trafficEdgeCache = {}; // key → {p1, p2, midX, midY} +let _trafficFlowLayer = null; +let _trafficDashOffset = 0; +let _trafficDashId = null; +let _trafficActiveEdges = {}; function toggleTrafficPanel() { const panel = document.getElementById('saTrafficPanel'); @@ -2589,33 +2588,26 @@ function toggleTrafficPanel() { if (panel.classList.contains('open')) { trafficStop(); panel.classList.remove('open'); - _removeFlowCanvas(); + _removeFlowLayer(); } else { panel.classList.add('open'); loadTrafficData(); } } -function _ensureFlowCanvas() { +function _ensureFlowLayer() { 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); - 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; + if (!_trafficFlowLayer) { + _trafficFlowLayer = L.layerGroup().addTo(SA.map); + } + return _trafficFlowLayer; } -function _removeFlowCanvas() { +function _removeFlowLayer() { cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null; - if (_trafficCanvas) { _trafficCanvas.remove(); _trafficCanvas = null; _trafficCtx = null; } + cancelAnimationFrame(_trafficDashId); _trafficDashId = null; + if (_trafficFlowLayer) { SA.map.removeLayer(_trafficFlowLayer); _trafficFlowLayer = null; } + _trafficActiveEdges = {}; } function tsToBJ(ts) { const d = new Date(ts * 1000); return d.toISOString().replace('T',' ').slice(0,19); } @@ -2730,8 +2722,8 @@ function trafficPause() { cancelAnimationFrame(_trafficAnimId); _trafficAnimId = null; } function trafficStop() { - trafficPause(); _trafficData = null; _trafficEdgeCache = {}; - _removeFlowCanvas(); + trafficPause(); _trafficData = null; + _removeFlowLayer(); document.getElementById('saTrafficTime').textContent = '--:--:--'; document.getElementById('saTrafficInfo').textContent = ''; if (SA.pointLayer) SA.pointLayer.getLayers().forEach(l => { @@ -2770,100 +2762,93 @@ function trafficAnimate(timestamp) { } function renderTrafficCrossfade() { - const c = _ensureFlowCanvas(); - if (!c || !SA.map || !_trafficData) return; - const ctx = _trafficCtx; - if (!ctx) return; - ctx.clearRect(0, 0, c.width, c.height); + const layer = _ensureFlowLayer(); + if (!layer || !SA.map || !_trafficData) return; const wins = _trafficData.windows || []; if (!wins.length) return; - const coords = _trafficData.nodes_coords || {}; const mix = getWindowMix(); const w0 = wins[mix.i] || null; const w1 = mix.i + 1 < wins.length ? wins[mix.i + 1] : null; const t = mix.t; - // Collect edges from both windows with blend weights - // edgeKey → {source, target, weight, count} + // Build edge map with crossfade weights const edgeMap = {}; - function addEdges(edges, weight, baseCount) { + function add(edges, w) { if (!edges) return; edges.forEach(e => { const k = e.source + '|' + e.target; if (!edgeMap[k]) edgeMap[k] = {source:e.source, target:e.target, weight:0, count:0}; - edgeMap[k].weight = Math.max(edgeMap[k].weight, weight); - edgeMap[k].count = Math.max(edgeMap[k].count, e.count || 0); + edgeMap[k].weight = Math.max(edgeMap[k].weight, w); + edgeMap[k].count = Math.max(edgeMap[k].count, e.count||0); }); } - addEdges(w0 ? w0.edges : [], 1 - t, 0); - addEdges(w1 ? w1.edges : [], t, 0); - // Also add edges from next window for smooth fade-in - if (mix.i + 2 < wins.length) { - addEdges(wins[mix.i + 2].edges, Math.max(0, t - 1), 0); - } + add(w0?w0.edges:[], 1-t); + add(w1?w1.edges:[], t); const edges = Object.values(edgeMap); - if (!edges.length) return; - const maxCount = Math.max(1, ...edges.map(e => e.count)); - - // Active node set (union of both windows, weighted) const activeNodes = new Set(); - if (w0 && 1 - t > 0.1) (w0.active_nodes||[]).forEach(n => activeNodes.add(n)); + 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)); document.getElementById('saTrafficInfo').textContent = edges.length + ' 边 · ' + activeNodes.size + ' 节点 · ' + tsToBJ(_trafficCurrentTime); - // Build pixel coordinates for edges (cached until map moves) + // Build/track Leaflet polylines with dash animation + const newKeys = new Set(); 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 k = e.source + '|' + e.target; + newKeys.add(k); const ratio = Math.min(1, e.count / maxCount); const alpha = 0.4 + 0.6 * e.weight; - const hue = 220 + (mix.i % 10) * 15; // color shifts per window group + const w = 1.5 + 4 * ratio; + const srcC = coords[e.source], dstC = coords[e.target]; + if (!srcC || !dstC) return; - // Bezier trace - ctx.beginPath(); - ctx.moveTo(cached.p1x, cached.p1y); - ctx.quadraticCurveTo(cached.midX, cached.midY, cached.p2x, cached.p2y); - ctx.strokeStyle = 'hsla(' + hue + ',70%,50%,' + alpha.toFixed(2) + ')'; - ctx.lineWidth = 1.5 + 3 * ratio; - ctx.stroke(); - - // Particles (continuous, driven by Date.now) - const nParticles = Math.ceil(3 + 5 * ratio); - for (let pi = 0; pi < nParticles; pi++) { - const pt = ((Date.now() / 2000 + pi / nParticles) % 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; + 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}; } }); - // Invalidate edge cache if map has moved (simplified: clear on zoom/move) - SA.map.off('moveend.flowCache').on('moveend.flowCache', () => { _trafficEdgeCache = {}; }); + // Remove polylines no longer active + Object.keys(_trafficActiveEdges).forEach(k => { + if (!newKeys.has(k)) { + layer.removeLayer(_trafficActiveEdges[k].poly); + delete _trafficActiveEdges[k]; + } + }); + + // 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) {