DEV Community

Cover image for I Built a Real-Time Space Debris Tracker in a Single HTML File
shyn
shyn

Posted on

I Built a Real-Time Space Debris Tracker in a Single HTML File

There are over 27,000 tracked objects orbiting Earth right now. Defunct satellites, spent rocket stages, and millions of fragments from collisions and anti-satellite tests — all hurtling through Low Earth Orbit at speeds of up to 7.8 km/s. A fleck of paint at that velocity hits with the force of a thrown brick.

I wanted to visualise this. Not with a static infographic, but with a live, interactive tool that pulled real data, did real orbital maths, and let you click on individual objects and see exactly where they are right now.

The result is Nano Debris — a fully self-contained space debris monitoring dashboard in a single index.html file. No framework, no build step, no backend. Just HTML, CSS, vanilla JavaScript, and one CDN library.

This is the story of how it works, with a particular focus on the part that fascinated me most: teaching a web browser to do orbital mechanics.


What Even Is a TLE?

Before writing a line of code, I had to understand the data format the entire space tracking world runs on.

A Two-Line Element set (TLE) is a standardised format for describing a satellite's orbit. It was designed in the 1960s and it looks like this:

ISS (ZARYA)
1 25544U 98067A   24321.51389  .00018137  00000+0  32518-3 0  9993
2 25544  51.6416 240.3516 0002197  88.9461 271.1934 15.50124812479736
Enter fullscreen mode Exit fullscreen mode

Three lines. The first is the object name. Lines 1 and 2 contain the orbital parameters encoded in a dense fixed-width format. Hidden inside those numbers are things like:

  • Inclination (how tilted the orbit is relative to the equator)
  • Eccentricity (how elliptical vs circular the orbit is)
  • Mean motion (how many orbits per day — from which you can derive altitude)
  • Epoch (the reference time the measurements were taken)
  • BSTAR drag term (atmospheric drag coefficient — important for reentry prediction)

The key thing to understand is that a TLE is not a position. It's a recipe. You need a mathematical model to cook the recipe into an actual latitude, longitude, and altitude at a given moment in time.

That model is called SGP4.


SGP4 — The Maths Under the Hood

SGP4 (Simplified General Perturbations 4) is the propagator used by NORAD, NASA, and every serious space tracking system in the world. It takes a TLE and a timestamp and outputs a position and velocity vector in a coordinate system called ECI (Earth-Centred Inertial).

It accounts for things a simple Newtonian two-body model would miss:

  • J2 perturbation — Earth isn't a perfect sphere, it bulges at the equator, and this causes orbits to precess
  • Atmospheric drag — particularly important below 600 km where residual atmosphere gradually decays orbits
  • Solar radiation pressure — photons from the Sun exert a tiny but real force on satellites
  • Lunar and solar gravity — relevant for high orbits like GEO

Implementing SGP4 from scratch is a significant undertaking — the reference implementation is several hundred lines of Fortran translated from a 1980 paper. Fortunately, satellite.js is a well-maintained JavaScript port that's used by Heavens-Above, NASA's Eyes on the Solar System, and many others.

Loading it is one line:

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/satellite.js/4.1.3/satellite.min.js"
  integrity="sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="
  crossorigin="anonymous">
</script>
Enter fullscreen mode Exit fullscreen mode

Note the integrity attribute — that's a SHA-512 hash of the file. If the CDN were ever compromised and served a tampered version, the browser would refuse to execute it. Always use SRI hashes on CDN-loaded scripts.


Step 1: Fetching Real Data from CelesTrak

CelesTrak, maintained by Dr T.S. Kelso, is the gold standard free source for orbital data. It publishes regularly updated TLE sets for every tracked object and — crucially — offers a CORS-friendly JSON endpoint that a browser can hit directly with no proxy needed.

const FEEDS = [
  {
    key: 'station',
    url: 'https://celestrak.org/CCSDS/bulk.php?GROUP=stations&FORMAT=JSON',
    maxLoad: 20
  },
  {
    key: 'active',
    url: 'https://celestrak.org/CCSDS/bulk.php?GROUP=active&FORMAT=JSON',
    maxLoad: 300
  },
  {
    key: 'debris',
    url: 'https://celestrak.org/CCSDS/bulk.php?GROUP=cosmos-1408-debris&FORMAT=JSON',
    maxLoad: 200
  },
  {
    key: 'rocket',
    url: 'https://celestrak.org/CCSDS/bulk.php?GROUP=rocket-bodies&FORMAT=JSON',
    maxLoad: 150
  },
];
Enter fullscreen mode Exit fullscreen mode

Each record in the JSON response looks like this:

{
  "OBJECT_NAME": "ISS (ZARYA)",
  "NORAD_CAT_ID": "25544",
  "TLE_LINE1": "1 25544U 98067A ...",
  "TLE_LINE2": "2 25544  51.6416 ..."
}
Enter fullscreen mode Exit fullscreen mode

I parse each record into a satrec object — satellite.js's internal representation of the orbital parameters — and store it alongside metadata:

const sr = satellite.twoline2satrec(l1.trim(), l2.trim());
Enter fullscreen mode Exit fullscreen mode

If CelesTrak is unreachable (network issues, CORS problems, rate limiting), the app falls back to a hardcoded dataset of real TLEs for key objects. The user never sees a broken state.


Step 2: Turning TLEs Into Real Positions

This is where it gets interesting. Once you have a satrec, you can propagate it to any point in time:

function propagateAll(date) {
  const gmst = satellite.gstime(date); // Greenwich Mean Sidereal Time

  for (const obj of objects) {
    // Run SGP4 — returns position & velocity in ECI coordinates (km)
    const pv = satellite.propagate(obj.satrec, date);
    if (!pv.position || isNaN(pv.position.x)) continue;

    // Convert ECI → geodetic (lat/lon/altitude)
    const geo = satellite.eciToGeodetic(pv.position, gmst);
    obj.lat = satellite.degreesLat(geo.latitude);
    obj.lon = satellite.degreesLong(geo.longitude);
    obj.alt = Math.round(geo.height); // km above sea level
  }
}
Enter fullscreen mode Exit fullscreen mode

A few things worth unpacking here:

Why do we need GMST? ECI coordinates are fixed relative to the stars — they don't rotate with the Earth. To convert to lat/lon (which does rotate with the Earth), you need to know how far Earth has rotated since the reference epoch. Greenwich Mean Sidereal Time gives you exactly that angle.

Why might pv.position be undefined? SGP4 can fail for objects with very old TLEs (the propagation diverges), decayed objects, or corrupted data. The isNaN check catches silent numerical failures.

Why re-propagate every 10 seconds rather than every frame? SGP4 isn't expensive for a handful of objects, but at 670 objects running at 60fps it adds up. Positions change slowly enough that 10-second intervals are imperceptible visually and keep the CPU happy.


Step 3: Deriving Altitude Without Propagating

There's a handy shortcut for calculating a satellite's approximate altitude directly from its TLE, without running a full SGP4 propagation. Mean motion — the number of orbits per day — is encoded in Line 2, and from it you can derive the semi-major axis using Kepler's third law:

function altFromSatrec(satrec) {
  const mu = 398600.4418; // Earth's gravitational parameter, km³/s²
  const n = satrec.no / 60; // convert rad/min → rad/s
  const a = Math.cbrt(mu / (n * n)); // semi-major axis in km
  return Math.round(a - 6371); // subtract Earth's radius
}
Enter fullscreen mode Exit fullscreen mode

This gives a mean altitude accurate to within a few kilometres — good enough to classify objects into LEO (< 2,000 km), MEO (2,000–35,000 km), and GEO (~35,786 km) without a full position computation.


Step 4: Rendering on a 3D Globe

With positions computed, the rendering problem is projecting 3D spherical coordinates onto a 2D canvas. The approach is a simple orthographic projection with manual rotation state:

function proj(theta, phi, r) {
  // Spherical → Cartesian
  const x3 = r * Math.cos(phi) * Math.cos(theta);
  const y3 = r * Math.sin(phi);
  const z3 = r * Math.cos(phi) * Math.sin(theta);

  // Rotate by user's drag input (rotX, rotY)
  const x2 = x3 * Math.cos(rotY) - z3 * Math.sin(rotY);
  const z2 = x3 * Math.sin(rotY) + z3 * Math.cos(rotY);
  const y2 = y3 * Math.cos(rotX) - z2 * Math.sin(rotX);
  const z1 = y3 * Math.sin(rotX) + z2 * Math.cos(rotX);

  return { sx: cx + x2 * zoom * R, sy: cy - y2 * zoom * R, depth: z1 };
}
Enter fullscreen mode Exit fullscreen mode

The depth value (z1) tells us whether an object is on the near or far side of the globe — objects with negative depth are behind the Earth and get skipped. The remaining objects are sorted by depth so closer ones render on top.


Step 5: The Reentry Predictor

One of the most useful features for operators is knowing roughly when an object will reenter the atmosphere. Real prediction uses atmospheric density models like NRLMSISE-00 and integrates the drag equation over time. For a browser-based tool, a simplified altitude-band heuristic gives surprisingly useful results:

function predictReentry(obj) {
  const alt = obj.alt;

  if (alt > 1000) return { label: '> 1,000 years', color: '#22c55e' };
  if (alt > 600)  return { label: '50–200 years',  color: '#22c55e' };
  if (alt > 400)  return { label: '2–10 years',    color: '#eab308' };
  if (alt > 300)  return { label: '1–3 years',     color: '#f97316' };
  return           { label: '< 1 year',            color: '#ef4444' };
}
Enter fullscreen mode Exit fullscreen mode

This reflects a genuine physical reality. Above 1,000 km, atmospheric drag is negligible and objects persist for centuries — the Fengyun-1C debris cloud at 865 km won't fully clear until the 2030s. Below 400 km, drag becomes significant enough to cause reentry within years. Below 300 km, you're looking at months.


Step 6: The 90-Minute Ground Track

A ground track shows where a satellite has been and where it's going, projected onto the Earth's surface. Computing one is just SGP4 run repeatedly across a time window:

function computeGroundTrack(obj) {
  const points = [];
  const now = new Date();

  for (let m = -10; m <= 90; m += 1) {
    const t = new Date(now.getTime() + m * 60000);
    const pv = satellite.propagate(obj.satrec, t);
    const gmst = satellite.gstime(t);
    const geo = satellite.eciToGeodetic(pv.position, gmst);

    points.push({
      lat: satellite.degreesLat(geo.latitude),
      lon: satellite.degreesLong(geo.longitude),
      past: m < 0,
    });
  }

  return points;
}
Enter fullscreen mode Exit fullscreen mode

100 SGP4 calls, one per minute, covering 10 minutes of history and 90 minutes of forecast — roughly one full orbit for a typical LEO object. The past track renders at lower opacity than the forecast track, giving an instant visual sense of direction of travel.


What I Learned

Orbital mechanics is more accessible than it looks. SGP4 sounds intimidating but satellite.js abstracts the hard parts. The real challenge is understanding the coordinate systems — ECI vs ECEF vs geodetic — and when to convert between them. Once that clicks, the rest follows naturally.

Single-file apps are underrated for tools like this. No build pipeline means no deployment complexity. The entire tool is one file you can email, drop in a GitHub repo, or open directly from your desktop. For a data visualisation tool that doesn't need authentication or a database, it's the right architecture.

Security matters even in client-side tools. I patched six vulnerabilities after an audit: XSS via raw innerHTML interpolation of CelesTrak data, missing Subresource Integrity on the CDN script, no Content Security Policy, CSV injection in the export function, silent catch blocks swallowing errors, and throw 0 discarding stack traces. None of these would have been obvious bugs, but all of them would have been exploitable.

Real data changes the feel completely. The earlier version of this app used 2,200 randomly-placed simulated dots. The current version has ~670 real objects with real positions and real orbital parameters. It's a smaller number but it's immeasurably more meaningful — you can click on the ISS and see its actual latitude and longitude updated every 10 seconds.


Try It & Get the Code

The full source is on GitHub — one file, fully commented, MIT licensed:

👉 github.com/shynsec/nano-debris/

It runs directly in any modern browser. No install, no signup, no API key. If you're working in the space domain and want to extend it — more debris feeds, watchlists, pass timing for ground stations — the codebase is deliberately simple to hack on.

If you build something with it, I'd genuinely like to see it.


The conjunction risk figures shown in DebrisField are simulated using altitude-proximity heuristics for illustrative purposes. They are not operationally certified and should not be used for flight safety decisions.

Top comments (0)