How to Display Satellite Positions Using SGP4 Propagation in CesiumJS

The gods-eye-view repository implements a complete pipeline that parses Two-Line Element (TLE) data, propagates satellite states using the SGP4 algorithm via the satellite.js library, and renders real-time positions and orbit rings in CesiumJS using PointPrimitiveCollections and frame-synchronized model matrices.

The gods-eye-view open-source project demonstrates a production-grade approach to visualizing thousands of satellites in real-time within a CesiumJS viewer. By integrating the SGP4 (Simplified General Perturbations No. 4) propagation algorithm with efficient Cesium primitives, the system transforms raw orbital elements from CelesTrak into accurate geospatial coordinates. This guide examines the complete technical architecture, from TLE parsing in src/data/satellites.js to the per-frame update loops that maintain visualization performance.

Parsing TLE Data and Creating SATREC Objects

The pipeline begins by ingesting raw orbital data from the CelesTrak API endpoint /api/celestrak/*. The parseTLE helper function in src/data/satellites.js (lines 55-70) processes the text response into structured objects containing the satellite name and the two TLE lines.

// src/data/satellites.js#L55-L70
function parseTLE(rawText) {
  // Parses raw TLE text into array of {name, line1, line2} objects
  // Implementation handles line breaks and header stripping
}

Each parsed TLE pair is converted into a satrec structure using twoline2satrec from the satellite.js library. This occurs in the dense catalog loader around lines 2121-2123, where the system filters out any records containing propagation errors.

// src/data/satellites.js#L2121-L2123
const satrec = twoline2satrec(line1, line2);
if (satrec && satrec.error === 0) {
  catalog.set(Number(satrec.satnum), {name, satrec});
}

The satrec object encapsulates the orbital elements required for SGP4 propagation, including mean motion, eccentricity, and inclination.

Propagating Orbital Positions with SGP4

To obtain real-time coordinates, the propagatePosition function (lines 77-96) executes the SGP4 algorithm for a given epoch. This function calls propagate from satellite.js to generate Earth-Centered Inertial (ECI) position and velocity vectors, then converts these to geodetic coordinates.

// src/data/satellites.js#L77-L96
function propagatePosition(satrec, date) {
  const positionAndVelocity = propagate(satrec, date);
  const gmst = gstime(date);
  const position = eciToGeodetic(positionAndVelocity.position, gmst);
  
  return {
    longitude: degreesLong(position.longitude),
    latitude: degreesLat(position.latitude),
    altitude: position.height,
    speedMps: computeSpeed(positionAndVelocity.velocity)
  };
}

The conversion from ECI to geodetic coordinates uses the Greenwich Mean Sidereal Time (GMST) to account for Earth's rotation. This ensures the returned longitude, latitude, and altitude values align with the fixed Earth frame used by CesiumJS.

Rendering Static Orbit Rings

Visualizing orbital paths requires generating a full ring of positions. The computeOrbitPath function (lines 112-136) calculates one complete orbital period based on the satellite's mean motion, sampling positions at regular intervals to build a static geometry.

// src/data/satellites.js#L112-L136
function computeOrbitPath(satrec) {
  const periodMinutes = 1440 / satrec.no;  // no = mean motion (rad/min)
  const samples = 128;
  const positions = [];
  
  for (let i = 0; i <= samples; i++) {
    const minutes = (i / samples) * periodMinutes;
    const futureDate = new Date(now.getTime() + minutes * 60000);
    const pos = propagatePosition(satrec, futureDate);
    if (pos) {
      positions.push(Cesium.Cartesian3.fromDegrees(
        pos.longitude, pos.latitude, pos.altitude
      ));
    }
  }
  return positions;
}

The ring is baked at a fixed GMST to create an inertial frame representation. This static approach avoids recalculating the entire path every frame while maintaining mathematical accuracy.

Synchronizing Orbits with Earth Rotation

Because the orbit ring is constructed in an inertial frame but displayed in a rotating Earth-fixed frame, the system must compensate for GMST changes. The orbitFrameModelMatrix function (lines 441-453) computes a rotation matrix that aligns the baked ring with the current Earth orientation.

// src/data/satellites.js#L441-L453
function orbitFrameModelMatrix(satrec, referenceDate, currentDate) {
  const gmstRef = gstime(referenceDate);
  const gmstNow = gstime(currentDate);
  const deltaGMST = gmstNow - gmstRef;
  
  // Return rotation matrix compensating for elapsed Earth rotation
  return Cesium.Matrix3.fromRotationZ(deltaGMST);
}

This matrix is applied to the orbit ring primitive each frame, creating the visual effect that the ring remains attached to the moving satellite while the Earth rotates beneath it.

Real-Time Visualization Architecture

The rendering layer uses Cesium.PointPrimitiveCollection for efficient batch rendering of satellite positions. Individual satellites are added as point primitives with styling defined in POINT_STYLES, while the orbit ring exists as a single Cesium.Primitive updated via model matrix transformations.

The _showOrbitPath method (lines 557-586) manages the creation of these ring primitives, storing them for later rotation updates.

Performance optimization relies on the _preRenderTick function (lines 222-256), which drives the update loop:

  • Core fleet updates occur every POSITION_UPDATE_MS (1000ms) via _propagateAll
  • Dense catalog slicing uses a round-robin approach in _propagateDenseChunk to distribute thousands of extra satellites (like Starlink) across multiple frames
  • Tracked satellite positions refresh every frame via _getTrackedFramePosition
  • Orbit ring rotations update once per second through _updateOrbitPathRotations

Implementing Satellite Tracking

User interaction begins with picking detection on the PointPrimitiveCollection. When clicked, the _trackSatellite method (lines 544-574) hides the static point primitive and creates a dynamic Cesium.Entity with a callback property that samples SGP4 propagation each frame.

// src/data/satellites.js#L544-L574
_trackSatellite(satelliteId) {
  // Hide the point primitive
  this._pointCollection.get(satelliteId).show = false;
  
  // Create tracked entity with real-time position callback
  this._trackedEntity = this._viewer.entities.add({
    position: new Cesium.CallbackProperty((time) => {
      const pos = this._getTrackedFramePosition(satelliteId, time);
      return Cesium.Cartesian3.fromDegrees(
        pos.longitude, pos.latitude, pos.altitude
      );
    }, false),
    // Camera offset defined by TRACK_VIEW_FROM_LEO constant
    viewFrom: TRACK_VIEW_FROM_LEO
  });
}

This approach ensures the tracked satellite maintains precise orbital mechanics while the camera follows its trajectory.

Complete Implementation Example

The following example integrates the key components from gods-eye-view into a standalone CesiumJS application:

import { twoline2satrec, propagate, gstime, eciToGeodetic } from 'satellite.js';

// Initialize viewer
const viewer = new Cesium.Viewer('cesiumContainer');

// 1. Parse TLE data (reusing repo logic)
function parseTLE(rawText) {
  const lines = rawText.trim().split('\n');
  const satellites = [];
  for (let i = 0; i < lines.length; i += 3) {
    if (lines[i].startsWith('1 ') && lines[i+1]?.startsWith('2 ')) {
      satellites.push({
        name: lines[i-1]?.trim() || 'Unknown',
        line1: lines[i].trim(),
        line2: lines[i+1].trim()
      });
    }
  }
  return satellites;
}

// 2. Build propagation catalog
function buildCatalog(tleEntries) {
  const catalog = new Map();
  for (const {name, line1, line2} of tleEntries) {
    const satrec = twoline2satrec(line1, line2);
    if (satrec.error === 0) {
      catalog.set(Number(satrec.satnum), {name, satrec});
    }
  }
  return catalog;
}

// 3. Propagate to geodetic coordinates
function propagatePosition(satrec, date) {
  const pv = propagate(satrec, date);
  if (!pv.position) return null;
  
  const gd = eciToGeodetic(pv.position, gstime(date));
  return {
    longitude: Cesium.Math.toDegrees(gd.longitude),
    latitude: Cesium.Math.toDegrees(gd.latitude),
    altitude: gd.height * 1000  // Convert km to meters
  };
}

// 4. Create point collection
const points = viewer.scene.primitives.add(
  new Cesium.PointPrimitiveCollection()
);

// 5. Initialize visualization
async function init() {
  const response = await fetch('/api/celestrak/visual');
  const tleData = parseTLE(await response.text());
  const catalog = buildCatalog(tleData);
  
  // Add initial points
  const now = new Date();
  for (const [id, {satrec}] of catalog) {
    const pos = propagatePosition(satrec, now);
    if (pos) {
      points.add({
        position: Cesium.Cartesian3.fromDegrees(
          pos.longitude, pos.latitude, pos.altitude
        ),
        pixelSize: 4,
        color: Cesium.Color.WHITE,
        id: id
      });
    }
  }
  
  // 6. Animation loop (re-propagate each second)
  let lastUpdate = 0;
  viewer.scene.preRender.addEventListener((scene, time) => {
    const now = Date.now();
    if (now - lastUpdate < 1000) return;
    lastUpdate = now;
    
    const date = new Date(now);
    for (let i = 0; i < points.length; i++) {
      const pt = points.get(i);
      const sat = catalog.get(pt.id);
      if (sat) {
        const pos = propagatePosition(sat.satrec, date);
        if (pos) {
          pt.position = Cesium.Cartesian3.fromDegrees(
            pos.longitude, pos.latitude, pos.altitude
          );
        }
      }
    }
  });
}

init();

This implementation follows the same architectural patterns found in src/data/satellites.js, utilizing the satellite.js library for SGP4 propagation and Cesium.PointPrimitiveCollection for GPU-efficient rendering.

Summary

  • TLE Parsing: The parseTLE function in src/data/satellites.js converts CelesTrak text into structured orbital elements, validated through twoline2satrec.
  • SGP4 Propagation: The propagatePosition function executes SGP4 calculations and converts ECI coordinates to geodetic longitude/latitude/altitude using GMST corrections.
  • Orbit Rings: computeOrbitPath generates static inertial-frame geometries, while orbitFrameModelMatrix applies rotational corrections to synchronize with Earth rotation.
  • Performance Architecture: The system uses Cesium.PointPrimitiveCollection for batch rendering and _preRenderTick to manage update frequency, including round-robin processing for dense satellite catalogs.
  • Tracking: Dynamic entities replace static primitives when selected, using callback properties to sample SGP4 propagation each frame for precise tracking.

Frequently Asked Questions

What is SGP4 and why is it required for satellite visualization?

SGP4 (Simplified General Perturbations No. 4) is a mathematical model used to propagate orbital state vectors from Two-Line Element sets. It accounts for Earth's oblateness (J2 perturbations), atmospheric drag, and gravitational effects to predict satellite positions with sufficient accuracy for visual tracking. According to the gods-eye-view implementation, the satellite.js library provides the twoline2satrec and propagate functions that implement this standard, ensuring compatibility with publicly available TLE data from CelesTrak.

How does the system maintain performance with thousands of satellites?

The repository implements a tiered update strategy within the _preRenderTick function. The core catalog updates every POSITION_UPDATE_MS (1000ms), while dense "Starlink-style" catalogs use _propagateDenseChunk to process subsets across multiple frames. Additionally, using Cesium.PointPrimitiveCollection instead of individual entities allows the GPU to render thousands of points in a single draw call, significantly improving frame rates compared to entity-based approaches.

Why are orbit rings baked at a fixed GMST instead of recalculated each frame?

Baking the orbit path at a fixed Greenwich Mean Sidereal Time creates a static inertial-frame geometry that represents the satellite's path through space without Earth's rotation. The orbitFrameModelMatrix function then applies a rotation matrix each frame to compensate for elapsed GMST. This approach is computationally efficient because it avoids recalculating 128+ propagation points every frame while maintaining visual accuracy as the Earth rotates beneath the fixed orbital plane.

Can I use custom TLE data instead of the CelesTrak API?

Yes. The parseTLE function in src/data/satellites.js accepts any standard TLE-formatted text string, regardless of source. You can replace the fetch call to /api/celestrak/* with a local file or alternative API endpoint. Ensure your TLE data follows the standard three-line format (satellite name, line 1, line 2) and that the twoline2satrec validation confirms satrec.error === 0 before adding satellites to the visualization catalog.

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 →