Track live aircraft on a 3D globe with Python and Cesium.
Welcome to another official UVF IT beginner build. Today we are constructing a minimal clone of a 'God's Eye View' style satellite map, focusing on the core experience: visualizing real-time public data on a photorealistic 3D Earth.
We will build your own live aircraft tracker inspired by open-source intelligence tools. Using Python for the backend and CesiumJS for the frontend, you will create a working system that fetches live ADS-B data and plots it on a globe in under 45 minutes.
Initialize a Python project using Flask to serve API endpoints. This backend will proxy requests to the OpenSky Network to avoid CORS issues in the browser.
Install dependencies and create the main application file. We will use requests to fetch data and sqlite3 for minimal state persistence.
setup.sh
mkdir satellite-viewer
cd satellite-viewer
python3 -m venv venv
source venv/bin/activate
pip install flask requestsapp.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/health')
def health():
return jsonify({"status": "ok"})
if __name__ == '__main__':
app.run(debug=True, port=5000)Implement the logic to fetch live aircraft data from OpenSky. We will limit the request to a specific bounding box (e.g., around London) to keep the payload small and responsive.
Parse the JSON response to extract only necessary fields: ICAO24 address, latitude, longitude, and callsign.
app.py
import requests
from flask import Flask, jsonify
app = Flask(__name__)
# OpenSky Network API credentials (use demo or your own)
OPENSKY_USER = 'demo'
OPENSKY_PASS = 'demo'
# Bounding box for London (min_lat, min_lon, max_lat, max_lon)
LON_MIN = -0.5
LON_MAX = 0.5
LAT_MIN = 51.2
LAT_MAX = 51.6
@app.route('/health')
def health():
return jsonify({"status": "ok"})
@app.route('/aircraft')
def get_aircraft():
url = 'https://opensky-network.org/api/states/all'
params = {
'lamin': LAT_MIN,
'lomin': LON_MIN,
'lamax': LAT_MAX,
'lomax': LON_MAX
}
try:
response = requests.get(url, auth=(OPENSKY_USER, OPENSKY_PASS), params=params, timeout=10)
response.raise_for_status()
data = response.json()
# Parse relevant fields
aircraft_list = []
for state in data.get('states', []):
# state structure: [icao24, callsign, origin_country, time_position, last_contact,
# longitude, latitude, baro_altitude, on_ground, velocity,
# heading, vertical_rate, sensors, geo_altitude, squawk,
# spi, position_source]
icao24 = state[0]
callsign = state[1].strip() if state[1] else 'N/A'
longitude = state[5]
latitude = state[6]
if latitude and longitude:
aircraft_list.append({
'icao24': icao24,
'callsign': callsign,
'latitude': latitude,
'longitude': longitude
})
return jsonify({"aircraft": aircraft_list})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)Create a simple HTML file that uses CesiumJS, a powerful WebGL library for 3D globes. This will serve as our frontend.
Add JavaScript to fetch data from our local Flask API and render aircraft as billboards (icons) on the globe.
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minimal Satellite Viewer</title>
<!-- CesiumJS -->
<script src="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
body, html { margin: 0; padding: 0; height: 100%; width: 100%; }
#cesiumContainer { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<script>
// Initialize Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer', {
baseLayerPicker: false,
geocoder: false,
homeButton: false,
infoBox: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false
});
// Function to fetch and render aircraft
async function updateAircraft() {
try {
const response = await fetch('/aircraft');
const data = await response.json();
// Clear existing entities
viewer.entities.removeAll();
// Add new aircraft
data.aircraft.forEach(aircraft => {
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(aircraft.longitude, aircraft.latitude),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: aircraft.callsign,
font: '14px monospace',
fillColor: Cesium.Color.WHITE,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -9)
}
});
});
console.log(`Updated ${data.aircraft.length} aircraft`);
} catch (error) {
console.error('Error fetching aircraft:', error);
}
}
// Initial fetch
updateAircraft();
// Update every 5 seconds
setInterval(updateAircraft, 5000);
// Zoom to London
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(-0.1276, 51.5074, 100000),
orientation: {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-90),
roll: 0.0
}
});
</script>
</body>
</html>app.py
import requests
from flask import Flask, jsonify, render_template
app = Flask(__name__)
# OpenSky Network API credentials (use demo or your own)
OPENSKY_USER = 'demo'
OPENSKY_PASS = 'demo'
# Bounding box for London (min_lat, min_lon, max_lat, max_lon)
LON_MIN = -0.5
LON_MAX = 0.5
LAT_MIN = 51.2
LAT_MAX = 51.6
@app.route('/')
def index():
return render_template('index.html')
@app.route('/health')
def health():
return jsonify({"status": "ok"})
@app.route('/aircraft')
def get_aircraft():
url = 'https://opensky-network.org/api/states/all'
params = {
'lamin': LAT_MIN,
'lomin': LON_MIN,
'lamax': LAT_MAX,
'lomax': LON_MAX
}
try:
response = requests.get(url, auth=(OPENSKY_USER, OPENSKY_PASS), params=params, timeout=10)
response.raise_for_status()
data = response.json()
# Parse relevant fields
aircraft_list = []
for state in data.get('states', []):
# state structure: [icao24, callsign, origin_country, time_position, last_contact,
# longitude, latitude, baro_altitude, on_ground, velocity,
# heading, vertical_rate, sensors, geo_altitude, squawk,
# spi, position_source]
icao24 = state[0]
callsign = state[1].strip() if state[1] else 'N/A'
longitude = state[5]
latitude = state[6]
if latitude and longitude:
aircraft_list.append({
'icao24': icao24,
'callsign': callsign,
'latitude': latitude,
'longitude': longitude
})
return jsonify({"aircraft": aircraft_list})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)With the backend and frontend files in place, it is time to launch your local development server. We will use a simple bash script to start the Flask application, which will serve both the API endpoints and the HTML interface.
Ensure you are in the project root directory. Running the server will bind to localhost on port 5000, allowing your browser to communicate with the Python backend without CORS issues.
run.sh
#!/bin/bash
# Make sure you have installed dependencies: pip install flask requests
# Set environment variable for Flask to run in debug mode
export FLASK_APP=app.py
export FLASK_ENV=development
# Start the Flask server
echo "Starting God's Eye View server on http://localhost:5000..."
flask runNow that the server is running, verify the data pipeline by checking the raw JSON output from the API endpoint. This ensures your Python backend is successfully fetching and formatting data from OpenSky before the frontend even touches it.
Next, open your browser to http://localhost:5000. You should see a 3D globe centered on London. Wait a few seconds for the Cesium viewer to load and the JavaScript to fetch aircraft data. Red dots should appear, representing live flights. Hover over them to see callsigns.
To extend the clone, consider adding a SQLite database to log historical positions for flight path visualization. You could also enhance the UI by adding controls to zoom into specific aircraft or switch between different map imagery layers.
verify.sh
#!/bin/bash
# Verify the API is returning valid JSON with aircraft data
# This command fetches the last 5 aircraft in the London bounding box
echo "Fetching aircraft data from local API..."
curl -s http://localhost:5000/aircraft | python3 -m json.tool | head -n 20
echo ""
echo "Check your browser at http://localhost:5000 for the 3D globe view."| Check | Command / Why it matters |
|---|---|
| Flask server starts without errors | bash run.sh and check for 'Running on http://127.0.0.1:5000' in terminal |
| API returns JSON with aircraft data | curl http://localhost:5000/aircraft should return a JSON array with lat/lon fields |
| Browser shows 3D globe with red dots | Open http://localhost:5000 in Chrome/Firefox and visually confirm red markers over London |
| Dots update every 5 seconds | Observe movement or change in dot positions/callsigns over a 10-second period |
You have built a minimal live satellite viewer. To extend this, consider adding a SQLite database to store historical flight paths or integrating a map tile provider for higher resolution imagery.