Satellites overhead
About 190 satellites where they are right now: the two crewed space stations, the International Space Station and China's Tiangong, the brightest satellites in the night sky, including Hubble, and the GPS constellation. Click one to draw its path for one orbit. The ISS goes around in about an hour and a half, and a GPS satellite in twelve hours.
Build it yourself
CelesTrak publishes each satellite's orbit as two lines of numbers, a format called TLE. The satellite.js library works out where a satellite is at any moment from them, so the page updates every satellite's position each second without asking anything again. It was tested with MapLibre GL JS 6.15 and satellite.js 7.1 in Chromium.
What you need
Nothing but a web server to serve the page: the map tiles and CelesTrak's orbits need no key.
Download the orbits
Make a folder, and in it download CelesTrak's group of the brightest satellites, which includes the ISS, Tiangong, and Hubble:
curl -o visual.txt "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=tle"
Each satellite is three lines: its name, then the two lines of its orbit. CelesTrak updates its orbits every two hours and refuses computers that download the same group more often, so save the file once rather than fetching it each time the page loads. Orbits change, so download it again after a few days.
Make the page
Next to visual.txt, make index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Satellites overhead</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
</head>
<body style="margin: 0; background: #05070d">
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
import { degreesLat, degreesLong, eciToGeodetic, gstime, propagate, twoline2satrec } from "https://cdn.jsdelivr.net/npm/satellite.js@7.1.0/+esm";
const lines = (await (await fetch("visual.txt")).text()).trim().split(/\r?\n/);
const satellites = [];
for (let i = 0; i < lines.length; i += 3) {
satellites.push({ name: lines[i].trim(), satrec: twoline2satrec(lines[i + 1], lines[i + 2]) });
}
function where(satellite, date) {
const state = propagate(satellite.satrec, date);
if (!state?.position) return null;
const { longitude, latitude } = eciToGeodetic(state.position, gstime(date));
return [degreesLong(longitude), degreesLat(latitude)];
}
function positions() {
const now = new Date();
const features = satellites.flatMap((satellite, id) => {
const position = where(satellite, now);
if (!position) return [];
return [{ type: "Feature", properties: { id, name: satellite.name }, geometry: { type: "Point", coordinates: position } }];
});
return { type: "FeatureCollection", features };
}
function path(satellite) {
const coordinates = [];
const minutes = (2 * Math.PI) / satellite.satrec.no;
for (let minute = 0; minute <= minutes; minute++) {
const position = where(satellite, new Date(Date.now() + minute * 60000));
if (!position) continue;
const previous = coordinates.at(-1);
if (previous) position[0] += Math.round((previous[0] - position[0]) / 360) * 360;
coordinates.push(position);
}
return { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates } };
}
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/dark.json",
center: [-40, 20],
zoom: 2,
});
await map.once("load");
map.setProjection({ type: "globe" });
const iss = satellites.find((satellite) => satellite.name.startsWith("ISS"));
map.addSource("path", { type: "geojson", data: path(iss) });
map.addLayer({ id: "path", type: "line", source: "path", paint: { "line-color": "#ffd43b", "line-width": 1.5 } });
map.addSource("satellites", { type: "geojson", data: positions(), attribution: "Orbits: CelesTrak" });
map.addLayer({
id: "satellites",
type: "circle",
source: "satellites",
paint: { "circle-radius": 4, "circle-color": "#2a78d6", "circle-stroke-color": "#ffffff", "circle-stroke-width": 1 },
});
setInterval(() => map.getSource("satellites").setData(positions()), 1000);
map.on("click", "satellites", (event) => {
const { id, name } = event.features[0].properties;
map.getSource("path").setData(path(satellites[id]));
new maplibregl.Popup().setLngLat(event.lngLat).setText(name).addTo(map);
});
</script>
</body>
</html>
twoline2satrecreads each satellite's two lines into the record thatpropagateuses to find where it is at a given time.propagategives a position in space, andeciToGeodeticturns it into longitude and latitude over the turning Earth, using the time of day fromgstime.satrec.nois how far around its orbit a satellite goes each minute, in radians, so one orbit takes 2π divided by it.pathfinds a point for each minute of that, carrying longitudes past 180 so the line doesn't jump across the map.- Every second,
setDatareplaces the satellites' positions, and MapLibre redraws them.
Open it
Serve the folder with npx serve or python3 -m http.server, and open the address it prints. Opened straight from the file, the map stays blank, as Open the page explains. You should see about 150 satellites around a dark globe, and the ISS's next orbit in yellow. Click a satellite for its own.
CelesTrak's other groups include GPS, weather satellites, and Starlink. The map above adds the space stations and GPS, and saves a copy of each group when the site is built to fall back on.
Data
- Orbits: CelesTrak general perturbations elements, from the US Space Force's public catalog
- Positions: computed in the browser by satellite.js (MIT)
- Map: OpenStreetMap contributors (ODbL), in Mapricot's map tiles