Skip to content

Instantly share code, notes, and snippets.

@stormchasing
Last active November 13, 2025 01:56
Show Gist options
  • Select an option

  • Save stormchasing/e8779e89e02d7550d95b058d10c877b8 to your computer and use it in GitHub Desktop.

Select an option

Save stormchasing/e8779e89e02d7550d95b058d10c877b8 to your computer and use it in GitHub Desktop.
Userscript to display flight routes on ADSBExchange
// ==UserScript==
// @name ADSBExchange Route Display
// @namespace https://stormchasing.us/
// @version 1.7
// @description Display flight route from Google or Flightaware searches on ADSBExchange
// @author JR Hehnly (https://stormchasing.us/ @stormchasing)
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @match https://globe.adsbexchange.com/*
// @grant GM.xmlHttpRequest
// @connect google.com
// @connect flightaware.com
// ==/UserScript==
// Auto-synced from GitHub: https://gist.github.com/stormchasing/e8779e89e02d7550d95b058d10c877b8
(function() {
'use strict';
// Track the last processed callsign to avoid duplicate requests
let lastProcessedCallsign = null;
let routeDisplayElement = null;
/**
* Extract airport codes from Google search results
*/
function extractAirportCodesFromGoogle(htmlText) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
// Find all divs with class "Q1wtNe" - these should contain airport codes
const airportElements = doc.querySelectorAll('.Q1wtNe');
if (airportElements.length >= 2) {
const departureCode = airportElements[0].textContent.trim();
const arrivalCode = airportElements[1].textContent.trim();
// Basic validation - airport codes are usually 3-4 characters
if (departureCode.length >= 3 && departureCode.length <= 4 &&
arrivalCode.length >= 3 && arrivalCode.length <= 4) {
// Try to find city names in the bPxlCe div (format: "City to City")
let departureLocation = null;
let arrivalLocation = null;
const cityElement = doc.querySelector('.bPxlCe');
if (cityElement) {
const cityText = cityElement.textContent.trim();
const cityMatch = cityText.match(/^(.+?)\s+to\s+(.+)$/);
if (cityMatch) {
departureLocation = cityMatch[1].trim();
arrivalLocation = cityMatch[2].trim();
console.log(`ADSBExchange Route: Google cities found - ${departureLocation} to ${arrivalLocation}`);
}
}
return {
departure: {
code: departureCode,
name: null,
location: departureLocation
},
arrival: {
code: arrivalCode,
name: null,
location: arrivalLocation
}
};
}
}
return null;
}
/**
* Extract airport codes from FlightAware page
* FlightAware embeds flight data in a JavaScript variable called trackpollBootstrap
*/
function extractAirportCodesFromFlightAware(htmlText) {
let departure = null;
let arrival = null;
try {
// Look for the trackpollBootstrap JSON data embedded in the page
// We need to extract the JSON object carefully since it's nested
const trackpollStart = htmlText.indexOf('var trackpollBootstrap = ');
if (trackpollStart === -1) {
console.log('ADSBExchange Route: FlightAware trackpollBootstrap not found');
return null;
}
// Find the start of the JSON object
const jsonStart = htmlText.indexOf('{', trackpollStart);
if (jsonStart === -1) {
console.log('ADSBExchange Route: FlightAware JSON start not found');
return null;
}
// Find the matching closing brace by counting braces
let braceCount = 0;
let jsonEnd = -1;
for (let i = jsonStart; i < htmlText.length; i++) {
if (htmlText[i] === '{') braceCount++;
if (htmlText[i] === '}') {
braceCount--;
if (braceCount === 0) {
jsonEnd = i + 1;
break;
}
}
}
if (jsonEnd === -1) {
console.log('ADSBExchange Route: FlightAware JSON end not found');
return null;
}
const jsonString = htmlText.substring(jsonStart, jsonEnd);
console.log('ADSBExchange Route: FlightAware trackpollBootstrap found, parsing...');
const trackpollData = JSON.parse(jsonString);
// Navigate the JSON structure: flights -> first flight key -> activityLog -> flights array
if (trackpollData.flights) {
const flightKeys = Object.keys(trackpollData.flights);
if (flightKeys.length > 0) {
const firstFlightKey = flightKeys[0];
const flightData = trackpollData.flights[firstFlightKey];
// Check if flight tracking is blocked
if (flightData.blocked === true || flightData.blockedForUser === true) {
console.log('ADSBExchange Route: FlightAware - flight tracking is blocked');
return { blocked: true };
}
if (flightData.activityLog && flightData.activityLog.flights && flightData.activityLog.flights.length > 0) {
const flights = flightData.activityLog.flights;
let selectedFlight = null;
// Find the most relevant flight:
// 1. Look for currently airborne/enroute flights
// 2. If none, find the most recent flight with actual takeoff time
// 3. If none, use the most recent scheduled flight
// First, try to find an airborne flight
selectedFlight = flights.find(f =>
f.flightStatus === 'airborne' ||
f.flightStatus === 'enroute' ||
(f.takeoffTimes && f.takeoffTimes.actual && !f.landingTimes.actual)
);
// If no airborne flight, find the most recent flight with actual takeoff time
if (!selectedFlight) {
const flightsWithActual = flights.filter(f => f.takeoffTimes && f.takeoffTimes.actual);
if (flightsWithActual.length > 0) {
// Sort by actual takeoff time, descending (most recent first)
flightsWithActual.sort((a, b) => b.takeoffTimes.actual - a.takeoffTimes.actual);
selectedFlight = flightsWithActual[0];
}
}
// If still nothing, use the most recent scheduled flight
if (!selectedFlight) {
const flightsWithScheduled = flights.filter(f => f.takeoffTimes && f.takeoffTimes.scheduled);
if (flightsWithScheduled.length > 0) {
flightsWithScheduled.sort((a, b) => b.takeoffTimes.scheduled - a.takeoffTimes.scheduled);
selectedFlight = flightsWithScheduled[0];
}
}
// Fallback to first flight if no other criteria matched
if (!selectedFlight) {
selectedFlight = flights[0];
}
console.log('ADSBExchange Route: FlightAware selected flight status:', selectedFlight.flightStatus || 'scheduled');
// Extract origin and destination IATA codes and names
if (selectedFlight.origin && selectedFlight.origin.iata) {
departure = {
code: selectedFlight.origin.iata,
name: selectedFlight.origin.friendlyName || null,
location: selectedFlight.origin.friendlyLocation || null
};
console.log('ADSBExchange Route: FlightAware departure:', departure);
}
if (selectedFlight.destination && selectedFlight.destination.iata) {
arrival = {
code: selectedFlight.destination.iata,
name: selectedFlight.destination.friendlyName || null,
location: selectedFlight.destination.friendlyLocation || null
};
console.log('ADSBExchange Route: FlightAware arrival:', arrival);
}
}
}
}
// Validate airport codes
if (departure && arrival &&
departure.code && arrival.code &&
departure.code.length >= 3 && departure.code.length <= 4 &&
arrival.code.length >= 3 && arrival.code.length <= 4) {
console.log(`ADSBExchange Route: FlightAware validation passed - ${departure.code} to ${arrival.code}`);
return { departure, arrival };
}
console.log(`ADSBExchange Route: FlightAware validation failed - departure: "${departure}", arrival: "${arrival}"`);
return null;
} catch (error) {
console.error('ADSBExchange Route: Error parsing FlightAware data:', error);
return null;
}
}
/**
* Query FlightAware for flight route information
*/
function queryFlightAware(callsign) {
const flightAwareUrl = `https://www.flightaware.com/live/flight/${encodeURIComponent(callsign)}`;
console.log(`ADSBExchange Route: Trying FlightAware for ${callsign}`);
GM.xmlHttpRequest({
method: 'GET',
url: flightAwareUrl,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': 'https://www.flightaware.com/',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin'
},
onload: function(response) {
const airports = extractAirportCodesFromFlightAware(response.responseText);
if (airports) {
if (airports.blocked) {
console.log(`ADSBExchange Route: FlightAware reports tracking blocked for ${callsign}`);
displayRoute(null, null, true);
} else {
console.log(`ADSBExchange Route: Found route on FlightAware: ${airports.departure.code} - ${airports.arrival.code}`);
displayRoute(airports.departure, airports.arrival);
}
} else {
console.log(`ADSBExchange Route: Could not find airport codes on FlightAware for ${callsign}`);
displayRoute(null, null);
}
},
onerror: function(error) {
console.error('ADSBExchange Route: Error fetching FlightAware results', error);
displayRoute(null, null);
}
});
}
/**
* Query Google for flight route information
*/
function queryFlightRoute(callsign) {
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(callsign)}`;
GM.xmlHttpRequest({
method: 'GET',
url: searchUrl,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': 'https://www.google.com/',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin'
},
onload: function(response) {
const airports = extractAirportCodesFromGoogle(response.responseText);
if (airports) {
console.log(`ADSBExchange Route: Found route on Google: ${airports.departure} - ${airports.arrival}`);
displayRoute(airports.departure, airports.arrival);
} else {
console.log(`ADSBExchange Route: Could not find airport codes on Google for ${callsign}, trying FlightAware...`);
// Fallback to FlightAware
queryFlightAware(callsign);
}
},
onerror: function(error) {
console.error('ADSBExchange Route: Error fetching Google search results', error);
// Fallback to FlightAware on error
queryFlightAware(callsign);
}
});
}
/**
* Display the route information on the page
*/
function displayRoute(departure, arrival, blocked = false) {
// Find the identLarge span within the selected_infoblock
const infoBlock = document.querySelector('#selected_infoblock');
const callsignElement = infoBlock ? infoBlock.querySelector('.identLarge') : null;
if (!callsignElement) {
console.error('ADSBExchange Route: Could not find identLarge element in selected_infoblock');
return;
}
// Remove existing route display if present
if (routeDisplayElement && routeDisplayElement.parentNode) {
routeDisplayElement.parentNode.removeChild(routeDisplayElement);
}
// Create new route display div
routeDisplayElement = document.createElement('div');
routeDisplayElement.id = 'adsb_route_display';
routeDisplayElement.style.marginTop = '5px';
routeDisplayElement.style.fontSize = '14px';
routeDisplayElement.style.fontWeight = 'bold';
// Set content and color based on whether we found route info
if (blocked) {
// Display blocked message
routeDisplayElement.style.color = '#ff8800';
routeDisplayElement.textContent = 'Info Blocked';
} else if (departure && arrival) {
routeDisplayElement.style.color = '#00ff00';
// Create departure span with tooltip
const departureSpan = document.createElement('span');
departureSpan.textContent = departure.code;
departureSpan.style.cursor = 'help';
if (departure.name && departure.location) {
departureSpan.title = `${departure.name}\n${departure.location}`;
} else if (departure.location) {
// Just city/location without airport name
departureSpan.title = departure.location;
}
// Create separator
const separator = document.createTextNode(' - ');
// Create arrival span with tooltip
const arrivalSpan = document.createElement('span');
arrivalSpan.textContent = arrival.code;
arrivalSpan.style.cursor = 'help';
if (arrival.name && arrival.location) {
arrivalSpan.title = `${arrival.name}\n${arrival.location}`;
} else if (arrival.location) {
// Just city/location without airport name
arrivalSpan.title = arrival.location;
}
// Append to route display
routeDisplayElement.appendChild(departureSpan);
routeDisplayElement.appendChild(separator);
routeDisplayElement.appendChild(arrivalSpan);
} else {
routeDisplayElement.style.color = '#888888';
routeDisplayElement.textContent = 'No Route Info';
}
// Insert after the identLarge span's parent div, but within the highlightedTitle section
const parentDiv = callsignElement.parentElement;
const highlightedTitleSection = parentDiv.parentElement;
if (parentDiv && parentDiv.nextSibling) {
highlightedTitleSection.insertBefore(routeDisplayElement, parentDiv.nextSibling);
} else if (parentDiv) {
highlightedTitleSection.appendChild(routeDisplayElement);
}
}
/**
* Handle when the infoblock opens
*/
function handleInfoBlockOpen() {
const callsignElement = document.querySelector('#selected_callsign');
if (!callsignElement) {
console.log('ADSBExchange Route: Callsign element not found');
return;
}
const callsign = callsignElement.textContent.trim();
// Avoid processing the same callsign multiple times
if (callsign && callsign !== lastProcessedCallsign) {
console.log(`ADSBExchange Route: Processing callsign ${callsign}`);
lastProcessedCallsign = callsign;
queryFlightRoute(callsign);
}
}
/**
* Check if the infoblock is currently open (visible)
*/
function isInfoBlockOpen(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
return style.display !== 'none';
}
/**
* Initialize the script
*/
function init() {
const infoBlock = document.querySelector('#selected_infoblock');
if (!infoBlock) {
console.log('ADSBExchange Route: Waiting for page to load...');
setTimeout(init, 1000);
return;
}
console.log('ADSBExchange Route: Initialized');
// Track the previous display state
let wasOpen = isInfoBlockOpen(infoBlock);
// Create a MutationObserver to watch for style changes on the infoblock
const infoblockObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const isOpen = isInfoBlockOpen(infoBlock);
// Detect transition from closed to open
if (!wasOpen && isOpen) {
console.log('ADSBExchange Route: InfoBlock opened');
handleInfoBlockOpen();
}
wasOpen = isOpen;
}
});
});
// Start observing the infoblock for attribute changes
infoblockObserver.observe(infoBlock, {
attributes: true,
attributeFilter: ['style']
});
// Create a MutationObserver to watch for callsign text changes
const callsignElement = document.querySelector('#selected_callsign');
if (callsignElement) {
const callsignObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
// Only trigger if the infoblock is currently open
if (isInfoBlockOpen(infoBlock)) {
console.log('ADSBExchange Route: Callsign changed while infoblock open');
handleInfoBlockOpen();
}
}
});
});
// Observe changes to the callsign text
callsignObserver.observe(callsignElement, {
childList: true,
characterData: true,
subtree: true
});
}
// Also check if it's already open when the script loads
if (wasOpen) {
handleInfoBlockOpen();
}
}
// Start the script when the page is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment