diff --git a/simple_analysis/templates/simple_analysis/simple_analysis.html b/simple_analysis/templates/simple_analysis/simple_analysis.html index f632726..078b2ba 100644 --- a/simple_analysis/templates/simple_analysis/simple_analysis.html +++ b/simple_analysis/templates/simple_analysis/simple_analysis.html @@ -2753,32 +2753,61 @@ function trafficAnimate(timestamp) { _trafficAnimId = requestAnimationFrame(trafficAnimate); } -// ── Flow polylines system ── -let _trafficFlowLayer = null; // L.layerGroup for flowing polylines +// ── Flow canvas overlay ── +let _trafficFlowLayer = null; let _trafficFlowAnimId = null; -let _trafficFlowOffset = 0; +let _trafficCanvasCtx = null; function _ensureFlowLayer() { if (!SA.map) return null; if (_trafficFlowLayer) return _trafficFlowLayer; - _trafficFlowLayer = L.layerGroup().addTo(SA.map); - return _trafficFlowLayer; + // Insert canvas AFTER map container, positioned to cover the map + 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:99999;'; + // Insert after map container in DOM + mapEl.parentNode.insertBefore(c, mapEl.nextSibling); + // Size to match map + const size = SA.map.getSize(); + c.width = size.x; c.height = size.y; + c.style.width = size.x + 'px'; c.style.height = size.y + 'px'; + // Position overlay over map + const mapRect = mapEl.getBoundingClientRect(); + const parentRect = mapEl.parentNode.getBoundingClientRect(); + c.style.left = (mapRect.left - parentRect.left) + 'px'; + c.style.top = (mapRect.top - parentRect.top) + 'px'; + // Update on map changes + SA.map.on('resize move zoomend', () => { + 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'; + const mr = mapEl.getBoundingClientRect(); + const pr = mapEl.parentNode.getBoundingClientRect(); + c.style.left = (mr.left - pr.left) + 'px'; + c.style.top = (mr.top - pr.top) + 'px'; + }); + _trafficCanvasCtx = c.getContext('2d'); + _trafficFlowLayer = c; + return c; } function _removeFlowLayer() { if (_trafficFlowAnimId) { cancelAnimationFrame(_trafficFlowAnimId); _trafficFlowAnimId = null; } - if (_trafficFlowLayer && SA.map) { SA.map.removeLayer(_trafficFlowLayer); _trafficFlowLayer = null; } + if (_trafficFlowLayer) { _trafficFlowLayer.remove(); _trafficFlowLayer = null; _trafficCanvasCtx = null; } } function renderTrafficContinuous(totalSpan, minTs, maxTs) { - const layer = _ensureFlowLayer(); - if (!layer || !SA.map) return; + const c = _ensureFlowLayer(); + if (!c || !SA.map) return; const coords = _trafficData.nodes_coords || {}; const edges = Object.values(_trafficEdgeMap); if (!edges.length) return; const maxCount = Math.max(1, ...edges.map(e => e.count)); totalSpan = totalSpan || 30; + const ctx = _trafficCanvasCtx; + if (!ctx) return; const progress = Math.max(0, Math.min(1, (_trafficCurrentTime - minTs) / totalSpan)); const bandWidth = 0.3; @@ -2798,34 +2827,48 @@ function renderTrafficContinuous(totalSpan, minTs, maxTs) { document.getElementById('saTrafficInfo').textContent = `${activeEdges.length} 活跃边 · ${activeNodes.size} 活跃节点 · ${tsToBJ(_trafficCurrentTime)}`; - // Use Leaflet polylines with animated dash-array for visible flow effect - layer.clearLayers(); + ctx.clearRect(0, 0, c.width, c.height); + + // Draw bezier curves THEN particles activeEdges.slice(0, 300).forEach(e => { const srcC = coords[e.source]; const dstC = coords[e.target]; if (!srcC || !dstC) return; + const p1 = SA.map.latLngToContainerPoint([srcC[0], srcC[1]]); + const p2 = SA.map.latLngToContainerPoint([dstC[0], dstC[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 = e.count / maxCount; - const w = 2 + 5 * ratio; - L.polyline([[srcC[0], srcC[1]], [dstC[0], dstC[1]]], { - color: `hsl(${220 + 15 * ratio}, 70%, ${45 + 20 * ratio}%)`, - weight: w, opacity: 0.85, - dashArray: `${10 + 15 * ratio} ${10 + 15 * (1 - ratio)}`, - dashOffset: String(-_trafficFlowOffset), - interactive: false, bubblingMouseEvents: false, - }).addTo(layer); - }); - // Animate dash offset - if (!_trafficFlowAnimId) { - _trafficFlowAnimId = requestAnimationFrame(function loop() { - _trafficFlowOffset = (_trafficFlowOffset + 0.5) % 40; - if (_trafficFlowLayer) { - _trafficFlowLayer.getLayers().forEach(p => { - if (p.setStyle) p.setStyle({dashOffset: String(-_trafficFlowOffset)}); - }); - } - _trafficFlowAnimId = requestAnimationFrame(loop); - }); - } + // Background trace + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + ctx.quadraticCurveTo(midX, midY, p2.x, p2.y); + ctx.strokeStyle = `rgba(67,97,238,${0.4 + 0.5 * ratio})`; + ctx.lineWidth = 1.5 + 4 * ratio; + ctx.stroke(); + + // FLOWING PARTICLES + const particleCount = Math.ceil(3 + 5 * ratio); + for (let pi = 0; pi < particleCount; pi++) { + // Each particle at a different phase along the curve + const t = ((Date.now() / 2000 + pi / particleCount) % 1); + const px = (1-t)*(1-t)*p1.x + 2*(1-t)*t*midX + t*t*p2.x; + const py = (1-t)*(1-t)*p1.y + 2*(1-t)*t*midY + t*t*p2.y; + + // Color changes along the flow - start blue, end red + const r = Math.round(67 + (230 - 67) * t); + const gVal = Math.round(97 + (57 - 97) * t); + const bVal = Math.round(238 + (70 - 238) * t); + ctx.beginPath(); + ctx.arc(px, py, 2.5 + 3 * ratio, 0, Math.PI * 2); + ctx.fillStyle = `rgb(${r},${gVal},${bVal})`; + ctx.shadowColor = `rgba(${r},${gVal},${bVal},0.6)`; + ctx.shadowBlur = 4; + ctx.fill(); + ctx.shadowBlur = 0; + } + }); // Dim inactive nodes if (SA.pointLayer && !SA._clusterFocus) {