# How Satellite Propagation Works in God's Eye View: A Deep Dive into the SGP4 Pipeline

> Discover how satellite propagation works in God's Eye View. Learn to convert TLE data into geographic coordinates for 3D rendering with the SGP4 orbital propagator.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: deep-dive
- Published: 2026-09-06

---

**God's Eye View propagates satellite positions in real time using the SGP4 orbital propagator from satellite.js, converting TLE data into geographic coordinates for Cesium 3D rendering.**

This open-source visualization tool renders thousands of satellites on an interactive globe by combining classical orbital mechanics with modern web graphics. Understanding its propagation pipeline reveals how raw two-line element sets transform into smoothly animated orbital tracks.

## The Core Propagation Architecture

The satellite propagation system lives primarily in [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js). It orchestrates TLE parsing, SGP4 propagation, coordinate transformation, and Cesium rendering into a cohesive pipeline that updates every second.

### TLE Parsing and Satrec Creation

Before propagation can begin, orbital data must be converted into a format the SGP4 algorithm understands. The code imports `twoline2satrec` from satellite.js to transform TLE pairs into **satrec objects**—compact orbital element records that the propagator consumes.

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

// TLE lines from CelesTrak
const tleLine1 = '1 25544U 98067A   23215.52421948  .00014767  00000-0  26602-4 0  9997';
const tleLine2 = '2 25544  51.6416 288.2418 0001478  33.7972  64.2041 15.50995519397152';

// Create satrec once — this stores the orbital elements
const satrec = twoline2satrec(tleLine1, tleLine2);

```

The application fetches TLEs from CelesTrak in six core groups, with an optional dense Starlink group for high-density scenarios.

### SGP4 Propagation: The Heart of the System

Every `POSITION_UPDATE_MS` (1000ms), the code invokes `propagate(satrec, date)` to compute the satellite's position and velocity in **Earth-Centered Inertial (ECI) coordinates**. This implements the SGP4 (Simplified General Perturbations 4) algorithm—the standard for near-Earth orbit prediction.

```javascript
const POSITION_UPDATE_MS = 1000;

function updateSatellitePosition(satrec, date) {
  // SGP4 propagation: returns position (km) and velocity (km/s) in ECI frame
  const { position, velocity } = propagate(satrec, date);
  
  // position: { x, y, z } in kilometers
  // velocity: { x, y, z } in kilometers per second
  
  return { position, velocity };
}

```

The SGP4 algorithm accounts for:
- Earth's oblateness (J2, J3, J4 perturbations)
- Atmospheric drag effects
- Gravitational harmonics

These perturbations are encoded in the satrec's initialization parameters from the TLE epoch and BSTAR drag term.

### ECI to Geodetic Coordinate Conversion

Raw ECI coordinates cannot be rendered directly on a globe. The pipeline converts them to **geodetic latitude, longitude, and altitude** using `eciToGeodetic`, which requires the Greenwich Sidereal Time (GST) to account for Earth's rotation.

```javascript
import { gstime, eciToGeodetic, degreesLat, degreesLong } from 'satellite.js';

function getGeographicCoordinates(position, date) {
  // Greenwich Sidereal Time at this moment
  const gmst = gstime(date);
  
  // Convert ECI to geodetic (WGS-84)
  const geodetic = eciToGeodetic(position, gmst);
  
  return {
    latitude: degreesLat(geodetic.latitude),   // radians → degrees
    longitude: degreesLong(geodetic.longitude), // radians → degrees
    altitude: geodetic.height                    // kilometers above ellipsoid
  };
}

```

The `gstime` function calculates Earth's rotational angle relative to the vernal equinox, essential for aligning inertial coordinates with the rotating Earth.

## Rendering Pipeline: From Coordinates to Pixels

Once geographic coordinates are computed, the system feeds them to Cesium's entity system for visualization.

### Point Rendering for Live Positions

Each satellite becomes a Cesium **PointPrimitive** or **Entity** positioned via `Cartesian3.fromDegrees`:

```javascript
import * as Cesium from 'cesium';

function createSatelliteEntity(viewer, lon, lat, altitude, satrec) {
  return viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(lon, lat, altitude * 1000), // km to meters
    point: {
      pixelSize: satrec.satnum === '25544' ? 8 : 4, // ISS gets larger point
      color: Cesium.Color.YELLOW,
      outlineWidth: 1,
      outlineColor: Cesium.Color.BLACK
    },
    label: {
      text: satrec.satnum === '25544' ? 'ISS' : undefined,
      font: '12px sans-serif',
      fillColor: Cesium.Color.WHITE,
      pixelOffset: new Cesium.Cartesian2(0, -10)
    }
  });
}

```

### Orbital Path Generation

The system generates **predictive orbital tracks** by sampling future positions. The constant `ORBIT_PATH_STEPS` (180 points) defines the resolution, with each step separated by `POSITION_UPDATE_MS`:

```javascript
const ORBIT_PATH_STEPS = 180;

function generateOrbitPath(satrec, startDate) {
  const pathPoints = [];
  
  for (let step = 0; step < ORBIT_PATH_STEPS; step++) {
    const futureTime = new Date(startDate.getTime() + step * POSITION_UPDATE_MS);
    const { position } = propagate(satrec, futureTime);
    const gmst = gstime(futureTime);
    const geo = eciToGeodetic(position, gmst);
    
    pathPoints.push(Cesium.Cartesian3.fromDegrees(
      degreesLong(geo.longitude),
      degreesLat(geo.latitude),
      geo.height * 1000
    ));
  }
  
  return pathPoints;
}

// Render as polyline
viewer.entities.add({
  polyline: {
    positions: pathPoints,
    width: 1,
    material: Cesium.Color.CYAN.withAlpha(0.7),
    arcType: Cesium.ArcType.NONE
  }
});

```

This creates a trailing or leading orbital path that visualizes the trajectory without recalculating the full physics at render time.

## Ring Rotation and Time Synchronization

To maintain accurate orientation of pre-computed orbit elements, the system synchronizes with **Greenwich Sidereal Time** via a dedicated timer. The `RING_ROTATION_MS` interval (1000ms) ensures that orbital rings stay locked to Earth's rotational frame rather than drifting in inertial space.

```javascript
const RING_ROTATION_MS = 1000;

function updateRingOrientation(satrecs, viewer) {
  // Recompute GST-based orientation for all orbital rings
  const now = new Date();
  const currentGmst = gstime(now);
  
  // Update ring primitive matrices or recompute path geometries
  // Implementation depends on Cesium primitive type used
}

```

This separation of **propagation timing** (individual satellites) from **ring rotation timing** (collective orbital orientation) optimizes performance while maintaining physical accuracy.

## ISS Special Handling

The International Space Station (NORAD ID 25544) receives enhanced treatment in the propagation pipeline. In [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js), specific logic identifies this satellite by its catalog number:

```javascript
const ISS_NORAD_ID = '25544';

function isISS(satrec) {
  return satrec.satnum === ISS_NORAD_ID;
}

// Enhanced rendering for ISS
if (isISS(satrec)) {
  entity.point.pixelSize = 8;
  entity.label.text = 'ISS';
  entity.path.show = true; // Always show orbital path for ISS
}

```

The code also calculates upcoming visible passes via [`src/data/issPass.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/issPass.js), using the same propagation pipeline to determine when the ISS crosses the observer's horizon.

## Complete Propagation Example

Here's the integrated flow as implemented in the source:

```javascript
import { 
  twoline2satrec, 
  propagate, 
  eciToGeodetic, 
  gstime, 
  degreesLat, 
  degreesLong 
} from 'satellite.js';
import * as Cesium from 'cesium';

class SatellitePropagator {
  constructor(tleLine1, tleLine2) {
    this.satrec = twoline2satrec(tleLine1, tleLine2);
    this.entity = null;
  }
  
  update(viewer, date) {
    // SGP4 propagation step
    const { position, velocity } = propagate(this.satrec, date);
    
    // Skip if propagation failed (deorbited or epoch too far)
    if (!position) return false;
    
    // Coordinate transformation
    const gmst = gstime(date);
    const geo = eciToGeodetic(position, gmst);
    const lon = degreesLong(geo.longitude);
    const lat = degreesLat(geo.latitude);
    const alt = geo.height;
    
    // Cesium position update
    const cartesian = Cesium.Cartesian3.fromDegrees(lon, lat, alt * 1000);
    
    if (!this.entity) {
      this.entity = viewer.entities.add({
        position: cartesian,
        point: { pixelSize: 4, color: Cesium.Color.YELLOW }
      });
    } else {
      this.entity.position = cartesian;
    }
    
    return { longitude: lon, latitude: lat, altitude: alt, velocity };
  }
  
  generatePath(startDate, steps = 180, stepMs = 1000) {
    const points = [];
    for (let i = 0; i < steps; i++) {
      const t = new Date(startDate.getTime() + i * stepMs);
      const { position } = propagate(this.satrec, t);
      if (!position) continue;
      
      const gmst = gstime(t);
      const geo = eciToGeodetic(position, gmst);
      points.push(Cesium.Cartesian3.fromDegrees(
        degreesLong(geo.longitude),
        degreesLat(geo.latitude),
        geo.height * 1000
      ));
    }
    return points;
  }
}

```

## Key Files and Their Roles

| File | Function | Key Exports |
|------|----------|-------------|
| [`src/data/satellites.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/satellites.js) | Core propagation engine | TLE loading, satrec management, update loops |
| [`src/data/issPass.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/issPass.js) | ISS pass prediction | Horizon crossing calculations using same propagator |
| [`src/ui.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/ui.js) | Layer integration | Satellite layer toggle, visibility controls |
| [`src/main.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/main.js) | Application entry | Cesium viewer initialization, layer registration |

## Summary

- **SGP4 via satellite.js** powers all orbital predictions, converting TLE epochs to ECI coordinates through the `propagate()` function
- **Two-stage coordinate transformation** moves from ECI (propagator output) to geodetic (rendering input) using `eciToGeodetic` with `gstime`
- **1000ms update cycle** balances real-time responsiveness with computational efficiency for thousands of satellites
- **180-point orbital paths** provide predictive visualization without continuous re-propagation
- **Special ISS handling** demonstrates extensible architecture for targeting specific satellites with enhanced features

## Frequently Asked Questions

### What algorithm does God's Eye View use for satellite propagation?

The application uses the **SGP4 (Simplified General Perturbations 4)** algorithm implemented by the satellite.js library. This is the standard propagator for near-Earth orbits, handling Earth's oblateness and atmospheric drag effects encoded in TLE data.

### How accurate are the satellite positions shown?

SGP4 provides accuracy typically within **1–5 kilometers** for near-Earth orbits when using recent TLE data. Accuracy degrades as TLE epochs age beyond a few days, which is why God's Eye View fetches fresh TLEs from CelesTrak regularly.

### Why does the code use both ECI and geodetic coordinates?

**ECI (Earth-Centered Inertial)** coordinates simplify orbital mechanics calculations because they don't rotate with Earth. **Geodetic coordinates** (latitude, longitude, altitude) are required for rendering on Cesium's globe, which is fixed to Earth's rotating frame. The `gstime` function bridges these reference frames.

### Can the propagation timestep be changed from 1 second?

Yes—the `POSITION_UPDATE_MS` constant controls the update interval. Smaller values increase smoothness but multiply computational load; larger values improve performance at the cost of visual stutter. The 1000ms default optimizes for rendering thousands of satellites simultaneously.