diff --git a/public/js/app.js b/public/js/app.js index 66a8de3..7df6d18 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -137,6 +137,7 @@ let siteThemeQuery = null; // For media query cleanup let siteThemeHandler = null; let backgroundIntervals = []; // To track setIntervals + let lastMapDataHash = ''; // Cache for map rendering optimization // Load sort state from localStorage or use default let currentSort = { column: 'up', direction: 'desc' }; @@ -882,12 +883,46 @@ } }); } - function updateMap2D(servers) { - if (!myMap2D) return; + // Debounced version of the map update to avoid rapid succession calls + // Optimized map update with change detection and performance tuning + const updateMap2D = debounce(function (servers) { + if (!myMap2D || !servers) return; // Shift longitude for Pacific-centered view const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng); + // Create a data fingerprint to avoid redundant renders + // We only care about geographical positions and latency connectivity for the map + const dataFingerprint = JSON.stringify({ + serverCount: servers.length, + latencyCount: currentLatencies.length, + // Sample critical connectivity data (first/last items) + lHash: currentLatencies.length > 0 ? (currentLatencies[0].id + currentLatencies[currentLatencies.length-1].id) : '', + // Include theme to ensure theme toggle still triggers redraw + theme: document.documentElement.classList.contains('light-theme') ? 'light' : 'dark' + }); + + if (dataFingerprint === lastMapDataHash) { + return; + } + lastMapDataHash = dataFingerprint; + + // 1. Pre-build coord lookup for O(1) route lookup + const serverCoords = new Map(); + servers.forEach(s => { + if (s.lat && s.lng) { + const coords = [shiftLng(s.lng), s.lat]; + const city = (s.city || '').toLowerCase().trim(); + const country = (s.country || '').toLowerCase().trim(); + const countryName = (s.countryName || '').toLowerCase().trim(); + + if (city) serverCoords.set(city, coords); + if (country) serverCoords.set(country, coords); + if (countryName) serverCoords.set(countryName, coords); + } + }); + + // 2. Prepare geoData for scatter points const geoData = servers .filter(s => s.lat && s.lng) .map(s => ({ @@ -901,9 +936,10 @@ netTx: s.netTx })); - // Combine all series + // Start with the scatter series const finalSeries = [ { + id: 'nodes-scatter', type: 'scatter', coordinateSystem: 'geo', geoIndex: 0, @@ -914,11 +950,12 @@ shadowColor: 'rgba(6, 182, 212, 0.5)' }, data: geoData, - zlevel: 1 + zlevel: 1, + animation: false // Performance optimization } ]; - // Add latency routes if configured + // 3. Process latency routes with grouping if (currentLatencies && currentLatencies.length > 0) { const countryCoords = { 'china': [116.4074, 39.9042], @@ -950,130 +987,114 @@ 'seoul': [126.9780, 37.5665] }; - const getCoords = (name) => { - const lowerName = (name || '').toLowerCase().trim(); - if (countryCoords[lowerName]) return countryCoords[lowerName]; - - const s = servers.find(sv => - (sv.countryName || '').toLowerCase() === lowerName || - (sv.country || '').toLowerCase() === lowerName || - (sv.city || '').toLowerCase() === lowerName - ); - if (s && s.lng && s.lat) return [shiftLng(s.lng), s.lat]; - return null; - }; - const getShiftedCoords = (name) => { - const c = countryCoords[(name || '').toLowerCase().trim()]; - if (c) return [shiftLng(c[0]), c[1]]; - const raw = getCoords(name); - if (raw) return [shiftLng(raw[0]), raw[1]]; + const lower = (name || '').toLowerCase().trim(); + if (countryCoords[lower]) return [shiftLng(countryCoords[lower][0]), countryCoords[lower][1]]; + const sCoord = serverCoords.get(lower); + if (sCoord) return sCoord; return null; }; - // Group latency routes by path to handle overlap visually const routeGroups = {}; currentLatencies.forEach(route => { const start = getShiftedCoords(route.source); const end = getShiftedCoords(route.dest); if (start && end) { - // Canonical points for grouping (independent of direction) - // Sort by longitude then latitude const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const key = `${pts[0][0].toFixed(4)},${pts[0][1].toFixed(4)}_${pts[1][0].toFixed(4)},${pts[1][1].toFixed(4)}`; + const key = `${pts[0][0].toFixed(2)},${pts[0][1].toFixed(2)}_${pts[1][0].toFixed(2)},${pts[1][1].toFixed(2)}`; if (!routeGroups[key]) routeGroups[key] = []; routeGroups[key].push({ route, start, end }); } }); + const styleGroupedSeries = new Map(); + Object.keys(routeGroups).forEach(key => { - const routes = routeGroups[key]; - const count = routes.length; + const routesInGroup = routeGroups[key]; + const count = routesInGroup.length; - routes.forEach((item, i) => { + routesInGroup.forEach((item, i) => { const { route, start, end } = item; - - // Identify if the route is in the "canonical" direction - const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const isForward = (start === pts[0]); - - // Normalization and special routing rules const lowS = (route.source || '').toLowerCase().trim(); const lowD = (route.dest || '').toLowerCase().trim(); - const isSeattleJapan = ( (lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan' ) || - ( (lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan' ); + + const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const isForward = (start === pts[0]); + const isSeattleJapan = ((lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan') || + ((lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan'); - // Canonical hash for deterministic randomization (period, etc) const names = [lowS, lowD].sort(); const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); - let finalCurve = 0; + let finalCurve = 0.12; if (isSeattleJapan) { - // Special rule: Seattle ↔ Japan route curves upward (North) - // Based on user feedback that previous signs resulted in a downward curve, - // we flip them to ensure the intended upward arc. - const isWestbound = (lowS === 'seattle' || lowS === 'us seattle'); - finalCurve = isWestbound ? -0.2 : 0.2; - } else if (count === 1) { - // Subtle consistent curve for single routes to maintain parallelism - finalCurve = 0.12; - } else { - // Symmetrical fan-out for multiple lines between the same points + finalCurve = (lowS === 'seattle' || lowS === 'us seattle') ? -0.2 : 0.2; + } else if (count > 1) { const magnitude = 0.12 + Math.floor(i / 2) * 0.12; const spread = (i % 2 === 0) ? magnitude : -magnitude; finalCurve = isForward ? spread : -spread; } - finalSeries.push({ - type: 'lines', - coordinateSystem: 'geo', - zlevel: 2, - latencyLine: true, - effect: { - show: true, - period: 3 + ((routeHash + i) % 5), // Deterministic random period: 3-7s to vary start/speed - trailLength: 0.1, - color: 'rgba(99, 102, 241, 0.8)', - symbol: 'arrow', - symbolSize: 6 - }, - lineStyle: { - color: 'rgba(99, 102, 241, 0.3)', - width: 2, - curveness: finalCurve - }, - tooltip: { - formatter: () => { - const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...'; - return ` -