/** * PPF Proxy Map - Interactive visualization */ (function() { 'use strict'; // Theme toggle (shared with dashboard) var themes = ['dark', 'muted-dark', 'light']; function getTheme() { if (document.documentElement.classList.contains('light')) return 'light'; if (document.documentElement.classList.contains('muted-dark')) return 'muted-dark'; return 'dark'; } function setTheme(theme) { document.documentElement.classList.remove('light', 'muted-dark'); if (theme === 'light') document.documentElement.classList.add('light'); else if (theme === 'muted-dark') document.documentElement.classList.add('muted-dark'); try { localStorage.setItem('ppf-theme', theme); } catch(e) {} } function initTheme() { var saved = null; try { saved = localStorage.getItem('ppf-theme'); } catch(e) {} if (saved && themes.indexOf(saved) !== -1) { setTheme(saved); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { setTheme('light'); } var btn = document.getElementById('themeToggle'); if (btn) { btn.addEventListener('click', function() { var current = getTheme(); var idx = themes.indexOf(current); var next = themes[(idx + 1) % themes.length]; setTheme(next); }); } } document.addEventListener('DOMContentLoaded', initTheme); // Country coordinates (ISO 3166-1 alpha-2 -> [lat, lon]) var COORDS = { "AD":[42.5,1.5],"AE":[24,54],"AF":[33,65],"AG":[17.05,-61.8],"AL":[41,20], "AM":[40,45],"AO":[-12.5,18.5],"AR":[-34,-64],"AT":[47.33,13.33],"AU":[-27,133], "AZ":[40.5,47.5],"BA":[44,18],"BB":[13.17,-59.53],"BD":[24,90],"BE":[50.83,4], "BF":[13,-2],"BG":[43,25],"BH":[26,50.55],"BI":[-3.5,30],"BJ":[9.5,2.25], "BN":[4.5,114.67],"BO":[-17,-65],"BR":[-10,-55],"BS":[24.25,-76],"BT":[27.5,90.5], "BW":[-22,24],"BY":[53,28],"BZ":[17.25,-88.75],"CA":[60,-95],"CD":[-2.5,23.5], "CF":[7,21],"CG":[-1,15],"CH":[47,8],"CI":[8,-5],"CL":[-30,-71],"CM":[6,12], "CN":[35,105],"CO":[4,-72],"CR":[10,-84],"CU":[21.5,-80],"CV":[16,-24], "CY":[35,33],"CZ":[49.75,15.5],"DE":[51,9],"DJ":[11.5,43],"DK":[56,10], "DO":[19,-70],"DZ":[28,3],"EC":[-2,-77.5],"EE":[59,26],"EG":[27,30], "ER":[15,39],"ES":[40,-4],"ET":[8,38],"FI":[64,26],"FJ":[-18,175],"FR":[46,2], "GA":[-1,11.75],"GB":[54,-2],"GE":[42,43.5],"GH":[8,-2],"GM":[13.47,-16.57], "GN":[11,-10],"GQ":[2,10],"GR":[39,22],"GT":[15.5,-90.25],"GW":[12,-15], "GY":[5,-59],"HK":[22.25,114.17],"HN":[15,-86.5],"HR":[45.17,15.5], "HT":[19,-72.42],"HU":[47,20],"ID":[-5,120],"IE":[53,-8],"IL":[31.5,34.75], "IN":[20,77],"IQ":[33,44],"IR":[32,53],"IS":[65,-18],"IT":[42.83,12.83], "JM":[18.25,-77.5],"JO":[31,36],"JP":[36,138],"KE":[-1,38],"KG":[41,75], "KH":[13,105],"KM":[-12.17,44.25],"KP":[40,127],"KR":[37,127.5],"KW":[29.5,45.75], "KZ":[48,68],"LA":[18,105],"LB":[33.83,35.83],"LK":[7,81],"LR":[6.5,-9.5], "LS":[-29.5,28.5],"LT":[56,24],"LU":[49.75,6.17],"LV":[57,25],"LY":[25,17], "MA":[32,-5],"MC":[43.73,7.42],"MD":[47,29],"ME":[42.5,19.3],"MG":[-20,47], "MK":[41.83,22],"ML":[17,-4],"MM":[22,98],"MN":[46,105],"MO":[22.17,113.55], "MR":[20,-12],"MT":[35.83,14.58],"MU":[-20.28,57.55],"MV":[3.25,73], "MW":[-13.5,34],"MX":[23,-102],"MY":[2.5,112.5],"MZ":[-18.25,35],"NA":[-22,17], "NE":[16,8],"NG":[10,8],"NI":[13,-85],"NL":[52.5,5.75],"NO":[62,10], "NP":[28,84],"NZ":[-41,174],"OM":[21,57],"PA":[9,-80],"PE":[-10,-76], "PG":[-6,147],"PH":[13,122],"PK":[30,70],"PL":[52,20],"PR":[18.25,-66.5], "PS":[32,35.25],"PT":[39.5,-8],"PY":[-23,-58],"QA":[25.5,51.25],"RO":[46,25], "RS":[44,21],"RU":[60,100],"RW":[-2,30],"SA":[25,45],"SC":[-4.58,55.67], "SD":[15,30],"SE":[62,15],"SG":[1.37,103.8],"SI":[46.12,14.82],"SK":[48.67,19.5], "SL":[8.5,-11.5],"SN":[14,-14],"SO":[10,49],"SR":[4,-56],"SS":[7,30], "SV":[13.83,-88.92],"SY":[35,38],"SZ":[-26.5,31.5],"TD":[15,19],"TG":[8,1.17], "TH":[15,100],"TJ":[39,71],"TM":[40,60],"TN":[34,9],"TO":[-20,-175], "TR":[39,35],"TT":[11,-61],"TW":[23.5,121],"TZ":[-6,35],"UA":[49,32], "UG":[1,32],"US":[38,-97],"UY":[-33,-56],"UZ":[41,64],"VE":[8,-66], "VN":[16,106],"YE":[15,48],"ZA":[-29,24],"ZM":[-15,30],"ZW":[-20,30],"XK":[42.6,20.9] }; // Country names var NAMES = { "AD":"Andorra","AE":"UAE","AF":"Afghanistan","AG":"Antigua","AL":"Albania", "AM":"Armenia","AO":"Angola","AR":"Argentina","AT":"Austria","AU":"Australia", "AZ":"Azerbaijan","BA":"Bosnia","BB":"Barbados","BD":"Bangladesh","BE":"Belgium", "BF":"Burkina Faso","BG":"Bulgaria","BH":"Bahrain","BI":"Burundi","BJ":"Benin", "BN":"Brunei","BO":"Bolivia","BR":"Brazil","BS":"Bahamas","BT":"Bhutan", "BW":"Botswana","BY":"Belarus","BZ":"Belize","CA":"Canada","CD":"DR Congo", "CF":"C. African Rep.","CG":"Congo","CH":"Switzerland","CI":"Ivory Coast", "CL":"Chile","CM":"Cameroon","CN":"China","CO":"Colombia","CR":"Costa Rica", "CU":"Cuba","CV":"Cape Verde","CY":"Cyprus","CZ":"Czechia","DE":"Germany", "DJ":"Djibouti","DK":"Denmark","DO":"Dominican Rep.","DZ":"Algeria","EC":"Ecuador", "EE":"Estonia","EG":"Egypt","ER":"Eritrea","ES":"Spain","ET":"Ethiopia", "FI":"Finland","FJ":"Fiji","FR":"France","GA":"Gabon","GB":"United Kingdom", "GE":"Georgia","GH":"Ghana","GM":"Gambia","GN":"Guinea","GQ":"Eq. Guinea", "GR":"Greece","GT":"Guatemala","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong", "HN":"Honduras","HR":"Croatia","HT":"Haiti","HU":"Hungary","ID":"Indonesia", "IE":"Ireland","IL":"Israel","IN":"India","IQ":"Iraq","IR":"Iran","IS":"Iceland", "IT":"Italy","JM":"Jamaica","JO":"Jordan","JP":"Japan","KE":"Kenya", "KG":"Kyrgyzstan","KH":"Cambodia","KM":"Comoros","KP":"North Korea", "KR":"South Korea","KW":"Kuwait","KZ":"Kazakhstan","LA":"Laos","LB":"Lebanon", "LK":"Sri Lanka","LR":"Liberia","LS":"Lesotho","LT":"Lithuania","LU":"Luxembourg", "LV":"Latvia","LY":"Libya","MA":"Morocco","MC":"Monaco","MD":"Moldova", "ME":"Montenegro","MG":"Madagascar","MK":"N. Macedonia","ML":"Mali","MM":"Myanmar", "MN":"Mongolia","MO":"Macau","MR":"Mauritania","MT":"Malta","MU":"Mauritius", "MV":"Maldives","MW":"Malawi","MX":"Mexico","MY":"Malaysia","MZ":"Mozambique", "NA":"Namibia","NE":"Niger","NG":"Nigeria","NI":"Nicaragua","NL":"Netherlands", "NO":"Norway","NP":"Nepal","NZ":"New Zealand","OM":"Oman","PA":"Panama", "PE":"Peru","PG":"Papua New Guinea","PH":"Philippines","PK":"Pakistan", "PL":"Poland","PR":"Puerto Rico","PS":"Palestine","PT":"Portugal","PY":"Paraguay", "QA":"Qatar","RO":"Romania","RS":"Serbia","RU":"Russia","RW":"Rwanda", "SA":"Saudi Arabia","SC":"Seychelles","SD":"Sudan","SE":"Sweden","SG":"Singapore", "SI":"Slovenia","SK":"Slovakia","SL":"Sierra Leone","SN":"Senegal","SO":"Somalia", "SR":"Suriname","SS":"South Sudan","SV":"El Salvador","SY":"Syria","SZ":"Eswatini", "TD":"Chad","TG":"Togo","TH":"Thailand","TJ":"Tajikistan","TM":"Turkmenistan", "TN":"Tunisia","TO":"Tonga","TR":"Turkey","TT":"Trinidad","TW":"Taiwan", "TZ":"Tanzania","UA":"Ukraine","UG":"Uganda","US":"United States","UY":"Uruguay", "UZ":"Uzbekistan","VE":"Venezuela","VN":"Vietnam","YE":"Yemen","ZA":"South Africa", "ZM":"Zambia","ZW":"Zimbabwe","XK":"Kosovo" }; // Anonymity color mapping (matches CSS variables) var ANON_COLORS = { elite: {fill: '#50c878', stroke: '#2d8a4e'}, anonymous: {fill: '#38bdf8', stroke: '#1d8acf'}, transparent: {fill: '#f97316', stroke: '#c2410c'}, unknown: {fill: '#6b7280', stroke: '#4b5563'} }; // Heatmap gradient var HEAT_GRADIENT = { 0.0: '#181f2a', 0.3: '#1d4e89', 0.5: '#38bdf8', 0.7: '#50c878', 1.0: '#7ee787' }; // Map configuration var MAP_CONFIG = { center: [30, 10], zoom: 2, minZoom: 2, maxZoom: 8, tileUrl: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', tileAttribution: '© OSM © CARTO' }; // Cluster configuration var CLUSTER_CONFIG = { showCoverageOnHover: false, spiderfyOnMaxZoom: true, disableClusteringAtZoom: 7, maxClusterRadius: 50 }; // DOM element references var $countryCount, $proxyCount, map, clusterGroup; /** * Initialize the map */ function init() { $countryCount = document.getElementById('countryCount'); $proxyCount = document.getElementById('proxyCount'); // Set loading state $countryCount.classList.add('loading'); $proxyCount.classList.add('loading'); // Create map map = L.map('map', { center: MAP_CONFIG.center, zoom: MAP_CONFIG.zoom, minZoom: MAP_CONFIG.minZoom, maxZoom: MAP_CONFIG.maxZoom, zoomControl: true, worldCopyJump: true }); // Add tile layer L.tileLayer(MAP_CONFIG.tileUrl, { attribution: MAP_CONFIG.tileAttribution, subdomains: 'abcd', maxZoom: 19 }).addTo(map); // Create cluster group clusterGroup = L.markerClusterGroup({ showCoverageOnHover: CLUSTER_CONFIG.showCoverageOnHover, spiderfyOnMaxZoom: CLUSTER_CONFIG.spiderfyOnMaxZoom, disableClusteringAtZoom: CLUSTER_CONFIG.disableClusteringAtZoom, maxClusterRadius: CLUSTER_CONFIG.maxClusterRadius, iconCreateFunction: createClusterIcon }); // Load data loadData(); } /** * Create cluster icon */ function createClusterIcon(cluster) { var count = cluster.getChildCount(); var size = count >= 100 ? 'lg' : count >= 10 ? 'md' : 'sm'; var sizeMap = {sm: 32, md: 40, lg: 50}; return L.divIcon({ html: '
', className: 'cluster-wrapper', iconSize: L.point(sizeMap[size], sizeMap[size]) }); } /** * Calculate brightness based on count (logarithmic scale) */ function calcBrightness(count, maxCount, minBrightness, maxBrightness) { minBrightness = minBrightness || 0.3; maxBrightness = maxBrightness || 1.0; var range = maxBrightness - minBrightness; return minBrightness + range * (Math.log(count + 1) / Math.log(maxCount + 1)); } /** * Calculate radius based on count */ function calcRadius(count, baseRadius, maxExtra, divisor) { baseRadius = baseRadius || 3; maxExtra = maxExtra || 7; divisor = divisor || 5; return baseRadius + Math.min(maxExtra, Math.sqrt(count / divisor)); } /** * Create popup content */ function createPopup(code, count, anon, lat, lon, isApprox) { var name = NAMES[code] || code; var anonLabel = anon ? anon.charAt(0).toUpperCase() + anon.slice(1) : ''; var coords = isApprox ? 'approx. location' : (anonLabel ? anonLabel + ' • ' : '') + lat.toFixed(1) + ', ' + lon.toFixed(1); return '