Last active
November 13, 2025 01:56
-
-
Save stormchasing/e8779e89e02d7550d95b058d10c877b8 to your computer and use it in GitHub Desktop.
Userscript to display flight routes on ADSBExchange
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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