(function (){
'use strict';
function initialize(root){
if(root.getAttribute('data-initialized')==='true') return;
root.setAttribute('data-initialized', 'true');
var targetInput=root.querySelector('[data-target]');
var maxHopsSelect=root.querySelector('[data-max-hop-select]');
var timeoutSelect=root.querySelector('[data-timeout-select]');
var runButton=root.querySelector('[data-run]');
var status=root.querySelector('[data-status]');
var alertBox=root.querySelector('[data-alert]');
var alertTitle=root.querySelector('[data-alert-title]');
var alertMessage=root.querySelector('[data-alert-message]');
var resultsSection=root.querySelector('[data-results]');
var routeList=root.querySelector('[data-route-list]');
var rawOutput=root.querySelector('[data-raw-output]');
var warningsBox=root.querySelector('[data-warnings]');
var healthBox=root.querySelector('[data-health]');
var loader=root.querySelector('[data-loader]');
var mapCanvas=root.querySelector('[data-map-canvas]');
var mapStatus=root.querySelector('[data-map-status]');
var statRoutes=root.querySelector('[data-stat-routes]');
var statHops=root.querySelector('[data-stat-hops]');
var statMapped=root.querySelector('[data-stat-mapped]');
var statUnmapped=root.querySelector('[data-stat-unmapped]');
var ajaxUrl=root.getAttribute('data-ajax-url')||'';
var nonce=root.getAttribute('data-nonce')||'';
var runButtonText=runButton ? runButton.textContent:'Run traceroute';
var storageKey='buyproxiesTracerouteMapForm';
var routes=[];
var warnings=[];
var stats={ routes: 0, hops: 0, mapped: 0, unmapped: 0 };
var leafletMap=null;
var leafletLayer=null;
var routeColors={
target: '#68bb03'
};
function clean(value){
return value===null||typeof value==='undefined' ? '':String(value).trim();
}
function sameText(first, second){
first=clean(first).toLowerCase();
second=clean(second).toLowerCase();
return first!==''&&first===second;
}
function routeTargetText(route){
var input=clean(route&&route.input);
var resolvedIp=clean(route&&route.resolved_ip);
if(!input) return resolvedIp||'-';
if(!resolvedIp||sameText(input, resolvedIp)) return input;
return input + ' -> ' + resolvedIp;
}
function setText(element, text){
if(element) element.textContent=text;
}
function escapeHtml(value){
return clean(value).replace(/[&<>"']/g, function (character){
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[character];
});
}
function routeColor(route){
return routeColors[route.id]||'#334155';
}
function rttNumber(hop){
var value=Number(hop&&hop.rtt_ms);
return isFinite(value) ? value:null;
}
function latencyClass(hop){
var value=rttNumber(hop);
if(value===null) return '';
if(value <=50) return 'is-fast';
if(value <=150) return 'is-medium';
return 'is-slow';
}
function setStatus(message, state){
if(!status) return;
status.textContent=message;
status.classList.toggle('is-error', state==='error');
status.classList.toggle('is-success', state==='success');
}
function showAlert(title, message){
if(!alertBox||!alertTitle||!alertMessage) return;
alertTitle.textContent=title||'Traceroute failed';
alertMessage.textContent=message||'';
alertBox.hidden=false;
}
function hideAlert(){
if(alertBox) alertBox.hidden=true;
}
function setLoading(loading){
root.classList.toggle('is-loading', loading);
if(loader) loader.hidden = !loading;
if(resultsSection&&loading) resultsSection.hidden=false;
if(resultsSection&&!loading&&!routes.length) resultsSection.hidden=true;
if(runButton){
runButton.disabled=loading;
runButton.textContent=loading ? 'Tracing route...':runButtonText;
}}
function readBoundedNumber(control, fallback, min, max, label){
var value=Number(control ? control.value:fallback);
if(!isFinite(value)||Math.floor(value)!==value){
return { error: label + ' must be a whole number.' };}
if(value < min){
return { error: label + ' must be at least ' + min + '.' };}
if(value > max){
return { error: label + ' cannot be higher than ' + max + '.' };}
return { value: value };}
function validateTraceControls(){
var maxHopsLimit=Number(root.getAttribute('data-max-hops'))||30;
var timeoutLimit=Number(root.getAttribute('data-timeout'))||5;
var maxHops=readBoundedNumber(maxHopsSelect, maxHopsLimit, 4, maxHopsLimit, 'Max hops');
var timeout=readBoundedNumber(timeoutSelect, timeoutLimit, 1, timeoutLimit, 'Timeout');
if(maxHops.error){
showAlert('Check max hops', maxHops.error);
setStatus('Check max hops', 'error');
if(maxHopsSelect) maxHopsSelect.focus();
return null;
}
if(timeout.error){
showAlert('Check timeout', timeout.error);
setStatus('Check timeout', 'error');
if(timeoutSelect) timeoutSelect.focus();
return null;
}
return {
maxHops: maxHops.value,
timeout: timeout.value
};}
function saveForm(){
try {
window.localStorage.setItem(storageKey, JSON.stringify({
target: targetInput ? targetInput.value:''
}));
} catch (error){}}
function restoreForm(){
var saved;
try {
saved=JSON.parse(window.localStorage.getItem(storageKey)||'{}');
} catch (error){
saved={};}
if(targetInput&&saved.target) targetInput.value=saved.target;
}
function hasCoordinates(hop){
var location=hop&&hop.location ? hop.location:{};
var lat=Number(location.latitude);
var lon=Number(location.longitude);
return Boolean(location.success&&isFinite(lat)&&isFinite(lon)&&lat >=-90&&lat <=90&&lon >=-180&&lon <=180);
}
function hopCoordinates(hop){
return {
lat: Number(hop.location.latitude),
lon: Number(hop.location.longitude)
};}
function locationText(location){
var parts=[];
if(!location||!location.success) return 'Unknown location';
[location.city, location.region, location.country].forEach(function (part){
part=clean(part);
if(part&&parts.indexOf(part)===-1) parts.push(part);
});
return parts.join(', ')||location.country_code||'Unknown location';
}
function countryCode(location){
return clean(location&&location.country_code).toUpperCase();
}
function flagImageUrl(location){
var code=countryCode(location).toLowerCase();
if(!/^[a-z]{2}$/.test(code)) return '';
return 'https://flagcdn.com/w40/' + encodeURIComponent(code) + '.png';
}
function createFlagElement(location){
var code=countryCode(location);
var url=flagImageUrl(location);
var flag;
if(!url) return null;
flag=document.createElement('span');
flag.className='traceroute-map__flag';
flag.style.backgroundImage='url("' + url + '")';
flag.setAttribute('role', 'img');
flag.setAttribute('aria-label', code + ' flag');
flag.title=code + ' flag';
return flag;
}
function flagHtml(location){
var code=countryCode(location);
var url=flagImageUrl(location);
if(!url) return '';
return '<span class="traceroute-map__flag" role="img" aria-label="' + escapeHtml(code + ' flag') + '" style="background-image:url(&quot;' + escapeHtml(url) + '&quot;)"></span>';
}
function networkText(location){
var parts=[];
if(!location||!location.success) return '';
if(location.isp) parts.push(location.isp);
if(location.organization&&location.organization!==location.isp) parts.push(location.organization);
if(location.asn) parts.push(/^AS/i.test(location.asn) ? location.asn.toUpperCase():'AS' + location.asn);
return parts.join(' / ');
}
function mapUrl(lat, lon, zoom){
return 'https://www.openstreetmap.org/?mlat=' + encodeURIComponent(lat) + '&mlon=' + encodeURIComponent(lon) + '#map=' + encodeURIComponent(zoom||6) + '/' + encodeURIComponent(lat) + '/' + encodeURIComponent(lon);
}
function ensureMap(){
if(!mapCanvas||!window.L) return null;
if(!leafletMap){
mapCanvas.textContent='';
leafletMap=window.L.map(mapCanvas, {
scrollWheelZoom: false,
attributionControl: true
});
window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '&copy; OpenStreetMap contributors'
}).addTo(leafletMap);
leafletLayer=window.L.layerGroup().addTo(leafletMap);
}
return leafletMap;
}
function clearMap(){
if(leafletLayer) leafletLayer.clearLayers();
}
function destroyMap(){
if(leafletMap&&leafletMap.remove){
leafletMap.remove();
}
leafletMap=null;
leafletLayer=null;
}
function routePoints(route){
return (route.hops||[]).filter(hasCoordinates).map(function (hop){
return {
route: route,
hop: hop,
coordinates: hopCoordinates(hop)
};});
}
function allRoutePoints(){
var points=[];
routes.forEach(function (route){
points=points.concat(routePoints(route));
});
return points;
}
function renderFallbackMap(points){
if(!mapCanvas) return;
mapCanvas.classList.add('is-fallback');
mapCanvas.textContent='';
points.forEach(function (point, index){
var x=Math.max(2, Math.min(98, ((point.coordinates.lon + 180) / 360) * 100));
var y=Math.max(2, Math.min(98, ((90 - point.coordinates.lat) / 180) * 100));
var pin=document.createElement('a');
var isDestination=index===points.length - 1;
var flagUrl=flagImageUrl(point.hop.location||{});
pin.className='traceroute-map__fallback-pin' + (isDestination&&flagUrl ? ' traceroute-map__fallback-pin--destination':'');
pin.href=mapUrl(point.coordinates.lat.toFixed(5), point.coordinates.lon.toFixed(5), 6);
pin.target='_blank';
pin.rel='noopener';
pin.style.left=x + '%';
pin.style.top=y + '%';
pin.style.setProperty('--route-color', routeColor(point.route));
pin.textContent=String(point.hop.hop);
if(isDestination&&flagUrl){
var badge=document.createElement('span');
badge.className='traceroute-map__fallback-flag-badge';
badge.style.backgroundImage='url("' + flagUrl + '")';
pin.appendChild(badge);
pin.setAttribute('aria-label', 'Destination hop ' + point.hop.hop);
}
pin.title=point.route.label + ' hop ' + point.hop.hop + ' - ' + point.hop.ip;
pin.style.setProperty('--pin-delay', String(Math.min(index, 18) * 35) + 'ms');
mapCanvas.appendChild(pin);
});
}
function renderLeafletMap(points){
var instance=ensureMap();
var bounds;
if(!instance) return false;
mapCanvas.classList.remove('is-fallback');
clearMap();
bounds=window.L.latLngBounds([]);
routes.forEach(function (route){
var pointsForRoute=routePoints(route);
var latLngs=pointsForRoute.map(function (point){
return [point.coordinates.lat, point.coordinates.lon];
});
pointsForRoute.forEach(function (point, pointIndex){
var location=point.hop.location||{};
var network=networkText(location);
var isDestination=pointIndex===pointsForRoute.length - 1;
var flagUrl=flagImageUrl(location);
var iconClass='traceroute-map__leaflet-marker' + (isDestination ? ' traceroute-map__leaflet-marker--destination':'');
var iconHtml='<span style="background:' + escapeHtml(routeColor(route)) + '">' + escapeHtml(point.hop.hop) + '</span>' +
(isDestination&&flagUrl ? '<i class="traceroute-map__destination-flag-badge" style="background-image:url(&quot;' + escapeHtml(flagUrl) + '&quot;)"></i>':'');
var hostname=clean(point.hop.rdns);
var popup='<strong>' + escapeHtml(route.label + ' hop ' + point.hop.hop) + '</strong><br>' +
escapeHtml(point.hop.ip) + '<br>' +
(hostname ? escapeHtml(hostname) + '<br>':'') +
flagHtml(location) + ' ' + escapeHtml(locationText(location)) + '<br>' +
(network ? escapeHtml(network) + '<br>':'') +
'<a href="' + escapeHtml(mapUrl(point.coordinates.lat.toFixed(6), point.coordinates.lon.toFixed(6), 7)) + '" target="_blank" rel="noopener">Open in OpenStreetMap</a>';
window.L.marker([point.coordinates.lat, point.coordinates.lon], {
icon: window.L.divIcon({
className: iconClass,
html: iconHtml,
iconSize: isDestination&&flagUrl ? [46, 34]:[30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -17]
})
}).addTo(leafletLayer).bindPopup(popup);
bounds.extend([point.coordinates.lat, point.coordinates.lon]);
});
if(latLngs.length > 1){
window.L.polyline(latLngs, {
color: routeColor(route),
weight: 3,
opacity: 0.78
}).addTo(leafletLayer);
}});
if(points.length===1){
instance.setView([points[0].coordinates.lat, points[0].coordinates.lon], 5);
}else if(bounds.isValid()){
instance.fitBounds(bounds, { padding: [34, 34], maxZoom: 6 });
}
window.setTimeout(function (){
instance.invalidateSize();
}, 80);
return true;
}
function renderMap(){
var points=allRoutePoints();
if(!mapCanvas) return;
if(!points.length){
destroyMap();
mapCanvas.classList.add('is-empty');
mapCanvas.classList.remove('is-fallback');
mapCanvas.textContent='No public hops with coordinates were returned.';
setText(mapStatus, 'Private, timed-out, or unmapped hops remain visible in the table.');
return;
}
mapCanvas.classList.remove('is-empty');
if(renderLeafletMap(points)){
setText(mapStatus, 'Click a hop marker to view IP, approximate location, network, and map link.');
}else{
renderFallbackMap(points);
setText(mapStatus, 'Map tiles could not load, so a coordinate fallback is shown.');
}}
function addCell(row, text, label, className){
var cell=document.createElement('td');
cell.textContent=clean(text)||'-';
if(label) cell.setAttribute('data-label', label);
if(className) cell.className=className;
row.appendChild(cell);
return cell;
}
function addRttCell(row, hop){
var display=clean(hop&&hop.rtt_ms);
var state=latencyClass(hop);
var cell=addCell(row, display ? display + ' ms':'', 'RTT', 'traceroute-map__rtt');
if(state) cell.classList.add(state);
return cell;
}
function addMapCell(row, hop){
var cell=document.createElement('td');
cell.setAttribute('data-label', 'Map');
if(hasCoordinates(hop)){
var coordinates=hopCoordinates(hop);
var link=document.createElement('a');
link.href=mapUrl(coordinates.lat.toFixed(6), coordinates.lon.toFixed(6), 7);
link.target='_blank';
link.rel='noopener';
link.textContent='Open map';
cell.appendChild(link);
}else{
cell.textContent=hop.map_status==='private' ? 'Private/reserved':(hop.timeout ? 'Timed out':'Unmapped');
}
row.appendChild(cell);
}
function addLocationCell(row, location){
var cell=document.createElement('td');
var content=document.createElement('span');
var text=document.createElement('span');
var flag=createFlagElement(location);
cell.className='traceroute-map__location-cell';
cell.setAttribute('data-label', 'Location');
content.className='traceroute-map__location-content';
if(flag) content.appendChild(flag);
text.textContent=locationText(location);
content.appendChild(text);
cell.appendChild(content);
row.appendChild(cell);
}
function routeMeta(route){
var hops=route.hops||[];
var mapped=hops.filter(hasCoordinates).length;
var reached=route.reached ? 'Reached':'Not confirmed';
return hops.length + ' hops | ' + mapped + ' mapped | ' + reached + ' | ' + (route.tool||'trace');
}
function computeHealth(){
var health={
reached: routes.length > 0,
slowest: null,
timeouts: 0,
privateHops: 0
};
routes.forEach(function (route){
if(!route.ok||!route.reached) health.reached=false;
(route.hops||[]).forEach(function (hop){
var rtt=rttNumber(hop);
if(hop.timeout||hop.map_status==='timeout') health.timeouts +=1;
if(hop.map_status==='private') health.privateHops +=1;
if(rtt!==null&&(!health.slowest||rtt > health.slowest.rtt)){
health.slowest={
hop: hop.hop,
ip: hop.ip,
rtt: rtt,
latencyClass: latencyClass(hop)
};}});
});
return health;
}
function appendHealthCard(label, value, detail, state){
var card=document.createElement('div');
var labelEl=document.createElement('span');
var valueEl=document.createElement('strong');
var detailEl=document.createElement('small');
card.className='traceroute-map__health-card' + (state ? ' ' + state:'');
labelEl.textContent=label;
valueEl.textContent=value;
detailEl.textContent=detail;
card.appendChild(labelEl);
card.appendChild(valueEl);
card.appendChild(detailEl);
healthBox.appendChild(card);
}
function renderHealth(){
var health;
if(!healthBox) return;
healthBox.textContent='';
if(!routes.length){
healthBox.hidden=true;
return;
}
health=computeHealth();
appendHealthCard(
'Destination',
health.reached ? 'Reached':'Not confirmed',
health.reached ? 'Final target replied in the trace.':'The trace ended before a final target reply.',
health.reached ? 'is-good':'is-watch'
);
appendHealthCard(
'Slowest hop',
health.slowest ? health.slowest.rtt + ' ms':'No RTT',
health.slowest ? 'Hop ' + health.slowest.hop + (health.slowest.ip ? ' - ' + health.slowest.ip:''):'No latency samples were parsed.',
health.slowest ? health.slowest.latencyClass:'is-watch'
);
appendHealthCard(
'Timeouts',
String(health.timeouts),
health.timeouts ? 'Routers may block or limit traceroute replies.':'No timed-out hop rows parsed.',
health.timeouts ? 'is-watch':'is-good'
);
appendHealthCard(
'Private hops',
String(health.privateHops),
health.privateHops ? 'Private or reserved hops are not mapped.':'No private/reserved hops parsed.',
health.privateHops ? 'is-watch':'is-good'
);
healthBox.hidden=false;
}
function createRouteButton(label, attribute, handler){
var button=document.createElement('button');
button.type='button';
button.className='traceroute-map__secondary traceroute-map__route-button';
button.textContent=label;
button.setAttribute(attribute, '');
button.addEventListener('click', handler);
return button;
}
function renderRoute(route){
var section=document.createElement('article');
var head=document.createElement('div');
var titleWrap=document.createElement('div');
var title=document.createElement('h3');
var subtitle=document.createElement('p');
var badge=document.createElement('strong');
var metaWrap=document.createElement('div');
var actions=document.createElement('div');
var tableWrap=document.createElement('div');
var table=document.createElement('table');
var thead=document.createElement('thead');
var tbody=document.createElement('tbody');
section.className='traceroute-map__route';
section.style.setProperty('--route-color', routeColor(route));
head.className='traceroute-map__route-head';
title.textContent=route.label;
subtitle.textContent=routeTargetText(route);
badge.textContent=route.ok ? routeMeta(route):'Trace unavailable';
badge.className=route.ok ? '':'is-error';
metaWrap.className='traceroute-map__route-meta';
actions.className='traceroute-map__route-actions';
titleWrap.appendChild(title);
titleWrap.appendChild(subtitle);
head.appendChild(titleWrap);
metaWrap.appendChild(badge);
if(route.ok){
actions.appendChild(createRouteButton('Copy summary', 'data-copy', function (){
copySummary(route);
}));
actions.appendChild(createRouteButton('Export CSV', 'data-export', function (){
exportCsv(route);
}));
metaWrap.appendChild(actions);
}
head.appendChild(metaWrap);
section.appendChild(head);
if(!route.ok){
var error=document.createElement('p');
error.className='traceroute-map__route-error';
error.textContent=route.error||'Traceroute could not run for this route.';
section.appendChild(error);
return section;
}
tableWrap.className='traceroute-map__table-wrap';
thead.innerHTML='<tr><th>Hop</th><th>IP</th><th>Host</th><th>RTT</th><th>Location</th><th>Network</th><th>Map</th></tr>';
table.appendChild(thead);
if(!(route.hops||[]).length){
var emptyRow=document.createElement('tr');
var emptyCell=document.createElement('td');
emptyCell.colSpan=7;
emptyCell.textContent='No hop rows could be parsed from the traceroute output.';
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
}
(route.hops||[]).forEach(function (hop){
var row=document.createElement('tr');
var location=hop.location||{};
row.className=hasCoordinates(hop) ? 'is-mapped':'';
addCell(row, hop.hop, 'Hop', 'traceroute-map__hop-num');
addCell(row, hop.ip||(hop.timeout ? '* * *':''), 'IP');
addCell(row, hop.rdns||'', 'Host', 'traceroute-map__host-cell');
addRttCell(row, hop);
addLocationCell(row, location);
addCell(row, networkText(location), 'Network');
addMapCell(row, hop);
tbody.appendChild(row);
});
table.appendChild(tbody);
tableWrap.appendChild(table);
section.appendChild(tableWrap);
return section;
}
function renderWarnings(){
if(!warningsBox) return;
warningsBox.textContent='';
if(!warnings.length){
warningsBox.hidden=true;
return;
}
warnings.forEach(function (warning){
var item=document.createElement('span');
item.textContent=warning;
warningsBox.appendChild(item);
});
warningsBox.hidden=false;
}
function renderRawOutput(){
if(!rawOutput) return;
rawOutput.textContent=routes.map(function (route){
var lines=[];
lines.push(route.label + ' (' + routeTargetText(route) + ')');
if(route.error) lines.push(route.error);
(route.raw||[]).forEach(function (line){
lines.push(line);
});
return lines.join('\n');
}).join('\n\n');
}
function renderStats(){
setText(statRoutes, stats.routes||routes.length||0);
setText(statHops, stats.hops||0);
setText(statMapped, stats.mapped||0);
setText(statUnmapped, stats.unmapped||0);
}
function render(){
if(routeList){
routeList.textContent='';
routes.forEach(function (route){
routeList.appendChild(renderRoute(route));
});
}
if(resultsSection) resultsSection.hidden = !routes.length;
renderStats();
renderHealth();
renderWarnings();
renderMap();
renderRawOutput();
}
function requestTrace(){
var target=targetInput ? targetInput.value.trim():'';
var formData=new window.FormData();
var controls;
hideAlert();
if(!target){
showAlert('Missing destination', 'Enter a public IP address or hostname to trace.');
setStatus('Enter a destination', 'error');
if(targetInput) targetInput.focus();
return;
}
controls=validateTraceControls();
if(!controls) return;
saveForm();
formData.append('action', 'traceroute_map_run');
formData.append('nonce', nonce);
formData.append('target', target);
formData.append('max_hops', String(controls.maxHops));
formData.append('timeout', String(controls.timeout));
setLoading(true);
setStatus('Tracing route...', '');
window.fetch(ajaxUrl, {
method: 'POST',
credentials: 'same-origin',
body: formData
}).then(function (response){
return response.json().catch(function (){
throw new Error('The server returned an invalid response.');
});
}).then(function (payload){
if(!payload||!payload.success){
throw new Error(payload&&payload.data&&payload.data.message ? payload.data.message:'Traceroute failed.');
}
routes=payload.data.routes||[];
warnings=payload.data.warnings||[];
stats=payload.data.stats||{};
render();
setStatus('Trace complete', 'success');
if(resultsSection){
try {
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (error){
resultsSection.scrollIntoView();
}}
}).catch(function (error){
showAlert('Traceroute failed', error.message||String(error));
setStatus('Trace failed', 'error');
}).finally(function (){
setLoading(false);
});
}
function routeCollection(route){
return route ? [route]:routes;
}
function summaryText(route){
var lines=[];
routeCollection(route).forEach(function (route){
lines.push(route.label + ': ' + routeTargetText(route));
if(route.error){
lines.push('  Error: ' + route.error);
return;
}
(route.hops||[]).forEach(function (hop){
var location=hop.location||{};
lines.push([
'  ' + hop.hop,
hop.ip||(hop.timeout ? '* * *':'unknown'),
hop.rdns||'',
hop.rtt_ms!=='' ? hop.rtt_ms + ' ms':'',
locationText(location),
networkText(location)
].filter(Boolean).join(' | '));
});
});
return lines.join('\n');
}
function copySummary(route){
var text=summaryText(route);
if(!text) return;
if(window.navigator.clipboard&&window.navigator.clipboard.writeText){
window.navigator.clipboard.writeText(text).then(function (){
setStatus('Summary copied', 'success');
}).catch(function (){
setStatus('Copy failed', 'error');
});
}else{
var textarea=document.createElement('textarea');
textarea.value=text;
textarea.setAttribute('readonly', 'readonly');
textarea.style.position='fixed';
textarea.style.left='-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand ('copy');
setStatus('Summary copied', 'success');
} catch (error){
setStatus('Copy failed', 'error');
}
document.body.removeChild(textarea);
}}
function csvLine(values){
return values.map(function (value){
return '"' + clean(value).replace(/"/g, '""') + '"';
}).join(',');
}
function exportCsv(route){
var rows=[csvLine(['Route', 'Hop', 'IP', 'Host', 'RTT ms', 'Location', 'Country Code', 'Latitude', 'Longitude', 'Network', 'ASN', 'Raw'])];
routeCollection(route).forEach(function (route){
(route.hops||[]).forEach(function (hop){
var location=hop.location||{};
rows.push(csvLine([
route.label,
hop.hop,
hop.ip,
hop.rdns||'',
hop.rtt_ms,
locationText(location),
location.country_code||'',
location.latitude||'',
location.longitude||'',
networkText(location),
location.asn||'',
hop.raw||''
]));
});
});
var blob=new window.Blob([rows.join('\n')], { type: 'text/csv;charset=utf-8' });
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download='traceroute-map.csv';
document.body.appendChild(link);
link.click();
window.setTimeout(function (){
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}, 0);
setStatus('CSV exported', 'success');
}
restoreForm();
if(runButton) runButton.addEventListener('click', requestTrace);
[targetInput, maxHopsSelect, timeoutSelect].forEach(function (field){
if(!field) return;
field.addEventListener('change', saveForm);
});
if(targetInput){
targetInput.addEventListener('keydown', function (event){
if(event.key==='Enter'){
event.preventDefault();
requestTrace();
}});
}}
function boot(){
Array.prototype.forEach.call(document.querySelectorAll('[data-traceroute-map]'), initialize);
}
if(document.readyState==='loading'){
document.addEventListener('DOMContentLoaded', boot);
}else{
boot();
}})();