How to Integrate Live Aircraft Data from OpenSky and adsb.lol into CesiumJS

You can integrate live aircraft data into CesiumJS by creating a server-side proxy to handle CORS and rate limits, normalizing the adsb.lol response to the OpenSky state-vector format, and rendering billboards through a Cesium primitive layer.

The Gods-Eye-View project demonstrates a production-ready pipeline for visualizing real-time flight data in CesiumJS. By combining the public adsb.lol API with OpenSky-compatible normalization found in the bilawalsidhu/gods-eye-view repository, you can display live aircraft positions with minimal latency and full control over rendering. This guide walks through the three-layer architecture—proxy, normalization, and rendering—that makes this integration possible.

Three-Layer Data Architecture

The implementation separates concerns into distinct layers to ensure the frontend remains agnostic of data source changes.

1. Server-Side Proxy Configuration

The first layer handles CORS restrictions and API rate limiting through a Vite development server proxy defined in vite.config.js. The adsbLolProxy() function intercepts requests to /api/adsblol/* and forwards them to https://api.adsb.lol, adding a custom User-Agent header and implementing an in-memory cache (_adsbLolPointCache) to prevent overwhelming the public API.

2. Data Normalization Layer

Raw adsb.lol responses follow a different schema than OpenSky. The src/data/adsbLolFallback.js module bridges this gap through normalizeAdsbLolAircraftState(), which extracts fields like hex identifier, latitude/longitude, altitude, speed, and track. This function applies unit conversions using constants such as KNOT_TO_MPS and FOOT_TO_M to ensure metric compatibility with CesiumJS. The normalizeAdsbLolPointResponse() wrapper bundles individual aircraft records into the standard OpenSky {time, states} structure expected by the rendering layer.

3. CesiumJS Rendering Layer

The final layer consumes normalized state vectors and manages visual primitives. The military flights implementation in src/data/militaryFlights.js demonstrates this by fetching /api/adsblol/mil, parsing the normalized payload, and executing reconcileVessels() to update Cesium BillboardCollection instances. This layer handles orientation-based rotation, speed-dependent scaling, and horizon occlusion using EllipsoidalOccluder.

Setting Up the Proxy in vite.config.js

To avoid CORS errors in the browser, you must route API requests through your development server. The proxy configuration intercepts frontend calls and manages caching logic.

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api/adsblol': {
        target: 'https://api.adsb.lol',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api\/adsblol/, ''),
        configure: (proxy, options) => {
          proxy.on('proxyReq', (proxyReq, req, res) => {
            proxyReq.setHeader('User-Agent', 'YourApp/1.0');
          });
        }
      }
    }
  }
});

This setup enables endpoints like /api/adsblol/mil for military traffic and /api/adsblol/point?lat=...&lon=...&radius=... for geographic queries, with automatic response caching to maintain UI responsiveness.

Normalizing adsb.lol Data to OpenSky Format

The adsb.lol API returns aircraft states in a proprietary format that must be transformed before CesiumJS can consume it consistently alongside other data sources.

// src/data/adsbLolFallback.js
import { KNOT_TO_MPS, FOOT_TO_M, FPM_TO_MPS } from './constants.js';

export function normalizeAdsbLolAircraftState(ac) {
  return [
    ac.hex,           // 0: ICAO24 address
    ac.flight?.trim(), // 1: Callsign
    null,             // 2: Origin country (not provided)
    null,             // 3: Time position
    null,             // 4: Last contact
    ac.lon,           // 5: Longitude
    ac.lat,           // 6: Latitude
    ac.alt_baro ? ac.alt_baro * FOOT_TO_M : null, // 7: Altitude (meters)
    ac.alt_baro === 'ground', // 8: On ground flag
    ac.gs ? ac.gs * KNOT_TO_MPS : null, // 9: Velocity (m/s)
    ac.track,         // 10: True track (degrees)
    null,             // 11: Vertical rate
    null,             // 12: Sensors
    null,             // 13: Geometric altitude
    null,             // 14: Squawk
    null,             // 15: Spi
    null              // 16: Position source
  ];
}

export function normalizeAdsbLolPointResponse(json) {
  return {
    time: json.now,
    states: json.aircraft.map(normalizeAdsbLolAircraftState)
  };
}

This normalization ensures that whether your data comes from adsb.lol or the standard OpenSky REST API, the Cesium rendering code receives an identical array structure.

Rendering Aircraft in CesiumJS

With normalized data in hand, you create and update Cesium primitives. The src/data/militaryFlights.js implementation shows how to maintain a collection of billboards that update every 60 seconds.

// src/data/militaryFlights.js
import { normalizeAdsbLolPointResponse } from './adsbLolFallback.js';

let billboardCollection = null;
const REFRESH_MS = 60000;

export function enable(viewer) {
  billboardCollection = viewer.scene.primitives.add(
    new Cesium.BillboardCollection({
      blendOption: Cesium.BlendOption.TRANSLUCENT
    })
  );
  
  loadLivePositions(viewer);
  setInterval(() => loadLivePositions(viewer), REFRESH_MS);
}

async function loadLivePositions(viewer) {
  try {
    const resp = await fetch('/api/adsblol/mil', { cache: 'no-store' });
    const raw = await resp.json();
    const { states } = normalizeAdsbLolPointResponse(raw);
    reconcileVessels(viewer, states);
  } catch (e) {
    console.error('Failed to load military flights:', e);
    // Graceful degradation: keep last known positions
  }
}

function reconcileVessels(viewer, states) {
  billboardCollection.removeAll();
  
  states.forEach(state => {
    const [hex, callsign, , , , lon, lat, altM, onGround, speed, track] = state;
    if (!lon || !lat) return;
    
    const position = Cesium.Cartesian3.fromDegrees(lon, lat, altM || 10000);
    
    billboardCollection.add({
      position,
      image: getAircraftIcon(onGround),
      rotation: Cesium.Math.toRadians(track || 0),
      scale: calculateScale(speed),
      id: hex,
      disableDepthTestDistance: Number.POSITIVE_INFINITY
    });
  });
}

The rendering layer handles focus deemphasis (dimming non-selected aircraft) and decluttering through the applyVesselFocusDeemphasis() helper, ensuring the visualization remains readable during high-density traffic scenarios.

Complete Integration Example

Here is a minimal implementation that wires all three layers together in a standalone Cesium application.

import { Viewer } from 'cesium';
import { normalizeAdsbLolPointResponse } from './src/data/adsbLolFallback.js';

const viewer = new Viewer('cesiumContainer');

async function fetchAndRenderAircraft(lat, lon, radiusNm = 250) {
  // 1. Fetch via proxy
  const url = `/api/adsblol/point?lat=${lat}&lon=${lon}&radius=${radiusNm}`;
  const resp = await fetch(url);
  const rawData = await resp.json();
  
  // 2. Normalize
  const { states } = normalizeAdsbLolPointResponse(rawData);
  
  // 3. Render
  const collection = viewer.scene.primitives.add(
    new Cesium.BillboardCollection()
  );
  
  states.forEach(state => {
    const [hex, , , , , lon, lat, altM, , speed, track] = state;
    collection.add({
      position: Cesium.Cartesian3.fromDegrees(lon, lat, altM || 0),
      image: 'aircraft-icon.svg',
      rotation: Cesium.Math.toRadians(track || 0),
      id: hex
    });
  });
}

// Initialize with San Francisco coordinates
fetchAndRenderAircraft(37.7749, -122.4194);

For a turnkey solution, import the existing military flights layer directly:

import militaryFlightsLayer from './src/data/militaryFlights.js';
militaryFlightsLayer.enable(viewer);

Summary

  • Proxy Layer: Configure vite.config.js with adsbLolProxy() to route /api/adsblol/* requests, handle CORS, and cache responses to protect against rate limiting.
  • Normalization: Use normalizeAdsbLolAircraftState() in src/data/adsbLolFallback.js to convert adsb.lol records into OpenSky-compatible state vectors with proper metric unit conversions.
  • Rendering: Implement a Cesium BillboardCollection manager (as shown in src/data/militaryFlights.js) that polls the proxy endpoint, reconciles vessel states, and handles visualization details like rotation, scaling, and occlusion.
  • Architecture: This three-layer pattern allows swapping data sources (OpenSky vs. adsb.lol) without modifying the Cesium rendering code.

Frequently Asked Questions

Do I need a server-side proxy to use adsb.lol with CesiumJS?

Yes. The adsb.lol API does not send CORS headers suitable for browser-based requests, and public(rate limits apply)[^1^]. The proxy in vite.config.js handles cross-origin restrictions, adds required User-Agent headers, and implements caching to prevent API bans during development.

What is the OpenSky state-vector format?

The OpenSky state-vector format is a standardized array where indices map to specific aircraft properties: index 0 is the ICAO24 hex address, 5 and 6 are longitude and latitude, 7 is geometric altitude in meters, 9 is velocity in meters per second, and 10 is true track in degrees. The normalizeAdsbLolPointResponse() function ensures adsb.lol data follows this exact schema.

How does the normalization handle unit conversions?

The normalizeAdsbLolAircraftState() function in src/data/adsbLolFallback.js multiplies raw adsb.lol values by conversion constants: KNOT_TO_MPS for ground speed, FOOT_TO_M for barometric altitude, and FPM_TO_MPS for vertical rates. This ensures all spatial data conforms to CesiumJS's metric coordinate system.

Can I use this architecture with the standard OpenSky API instead of adsb.lol?

Yes. The rendering layer in src/data/flights.js is designed to consume the normalized state-vector format regardless of source. You would replace the proxy target URL in vite.config.js and potentially skip the normalization step if the OpenSky API format matches your needs, though the existing normalizer provides a convenient abstraction for switching between feeds.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →