Code Clinic: Building a Starlink Constellation Tracker with Cesium
Propagating Orbits in Space from TLEs using Cesium and SatelliteJs
We sent these machines up to space to unlock the doors to the universe. With each mission, they ventured further into the unknown, charting a course for humanity’s expansion beyond Earth. Their courage and curiosity drove them to explore the cosmos, seeking answers to age-old questions and paving the way for future generations to follow. As they orbited our planet, gazing down upon the world without borders, they reminded us that we are all travelers on the same vessel, the spaceship Earth, bound for a destiny written in the stars. Their journey was not just a triumph of technology, but a testament to the indomitable human spirit that aspires to reach for the infinite.
I know this topic is only tangentially related to autonomous agents, but I really liked the exercise and am through my day job highly interested in the New Space Industry.
What I wanted to see was a simple visualization that allowed me to track Starlink’s satellite constellations in orbit.
Something like this.
So I went along and built it using SGP4 (Simplified General Perturbations 4), a mathematical model used to predict the position and velocity of an Earth-orbiting satellite given its Two-Line Element (TLE) data.
Two-Line Element (TLE) data originated in the early days (~1960s) of satellite tracking by NORAD (North American Aerospace Defense Command), aiming to provide a concise format for encoding orbital parameters of Earth-orbiting objects.
Prerequisites
We will be using TLE data for a selected Starlink constellation and push this data for propagation and visualization into two JavaScript libraries. There are Python implementations as well, but I wanted to use the already existing visualization capabilities from Cesium.
Data: TLEs from Celestrak
SGP4 Propagation: SatelliteJS
Visualization: Cesium
This write-up focuses only on the relevant JavaScript portion. The scripts for this can be downloaded after the paywall. If you need help with the implementation be sure to reach out.
Viewer Initialization:
A Cesium viewer is created with specified options, including the imagery provider and disabling various UI components like baseLayerPicker, geocoder, homeButton, etc.
The default tile textures are “Natural Earth II”, and it does the job well.
const viewer = new Cesium.Viewer('cesiumContainer', {
imageryProvider: new Cesium.TileMapServiceImageryProvider({
url: Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII"),
}),
baseLayerPicker: false, geocoder: false, homeButton: false, infoBox: false,
navigationHelpButton: false, sceneModePicker: false
});
TLE Data Definition:
Then we source the data from Celestrack in the form of Two-Line Elements for our Starlink constellation. In it’s original form, TLEs consist of two lines of data representing key orbital elements, enabling efficient communication and sharing of satellite orbit predictions among space agencies and satellite tracking organizations worldwide. The TLE format includes parameters like the satellite's mean motion, eccentricity, inclination, and argument of perigee, among others. But I do have an example below.
TLE is a data format encoding a list of orbital elements of an Earth-orbiting object for a given point in time.
For this exercise, I converted the text file into JSON format to make it easily readable and more secure in the process. More information about the LTE format can be found here. Then we calculate the orbit.
Satellite Orbit Calculation
The TLE data is used to calculate satellite orbits using the SatelliteJS library. A loop iterates over each satellite's TLE data to generate its orbit.
const satrec_array = [];
for (let i = 0; i < SATELLITES_TLE.length; i++) {
satrec_array.push(satellite.twoline2satrec(
SATELLITES_TLE[i]['data']['line1'].trim(),
SATELLITES_TLE[i]['data']['line2'].trim()
));
}
The key functionality here is that I select the lines from the JSON dataset and transfer them with the SatelliteJS library into a satellite orbit. Once this has been done, we need to set the clock for rendering the animation.
Clock and Time Management
Cesium clock settings are defined to control the animation of time over a specified period. The timeline is set to zoom to the specified start and stop times. The clock multiplier and range are set to control the speed and looping behavior of the animation. If the multiplier is set to “1”, then the tracking will happen in real-time.
const totalSeconds = 60 * 60 * 6;
const timestepInSeconds = 10;
const start = Cesium.JulianDate.fromDate(new Date());
const stop = Cesium.JulianDate.addSeconds(start, totalSeconds, new Cesium.JulianDate());
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.timeline.zoomTo(start, stop);
viewer.clock.multiplier = 5;
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
Now we have read our data, calculated our orbit, and initialized and configured our renderer. As a next step, we have to place the satellites on our 3-dimensional globe representation.
Satellite Position Calculation:
Here we have two loops. One loop iterates over the total number of satellites I want to track, another loop calculates the trajectory over time. SampledPositionProperty is instantiated to track the satellite's position at different times. Each satellite's trajectory is visualized on the globe with a blue dot and labeled with its name.
const sat_trajectories_array = [];
for (let i = 0; i < SATELLITES_TLE.length; i++) {
const sat_positionsOverTime = new Cesium.SampledPositionProperty();
for (let j = 0; j < totalSeconds; j += timestepInSeconds) {
const time = Cesium.JulianDate.addSeconds(start, j, new Cesium.JulianDate());
const jsDate = Cesium.JulianDate.toDate(time);
const positionAndVelocity = satellite.propagate(satrec_array[i], jsDate);
const gmst = satellite.gstime(jsDate);
const p = satellite.eciToGeodetic(positionAndVelocity.position, gmst);
const position = Cesium.Cartesian3.fromRadians(p.longitude, p.latitude, p.height * 1000);
sat_positionsOverTime.addSample(time, position);
}
const satellitePoint = viewer.entities.add({
position: sat_positionsOverTime,
label: { text: SATELLITES_TLE[i]["name"], font: '12px Helvetica' },
point: { pixelSize: 5, color: Cesium.Color.BLUE }
});
sat_trajectories_array.push(satellitePoint);
}
This part of the script is the main propagation and visualization “game loop”, as it is defined for each satellite in the constellation where it is supposed to be on a given day.
Camera Tracking:
Finally, the camera is set to follow the trajectory of a specific satellite (the first satellite in this case).
viewer.trackedEntity = sat_trajectories_array[0]
And that’s pretty much it for the deep tech parts.
Initialization and Zoom Out:
Now for the final step. Once the globe tiles are loaded, the camera zooms out and animation is enabled. And we get our nice visualization of a Starlink constellation in space.
let initialized = false;
viewer.scene.globe.tileLoadProgressEvent.addEventListener(() => {
if (!initialized && viewer.scene.globe.tilesLoaded === true) {
viewer.clock.shouldAnimate = true;
initialized = true;
viewer.scene.camera.zoomOut(7000000);
document.querySelector("#loading").classList.toggle('disappear', true);
}
});
Bonus - Entity Switching Function:
For a linear constellation, having the script just follow one of the Satellites is working. However, once you have more than one constellation or satellite you want to track, the visualization becomes nicer if it switches from one satellite to the next. Similar when you are in an Airplane.
Therefore I added the function switchToNextEntity
to switch between the tracked entities periodically.
function switchToNextEntity() {
savedCameraPosition = viewer.scene.camera.position.clone();
viewer.trackedEntity = sat_trajectories_array[currentIndex];
currentIndex = (currentIndex + 1) % sat_trajectories_array.length;
viewer.trackedEntity.viewFrom = savedCameraPosition;
}
I hope this exercise was valuable for you. Full downloaded after the paywall.
Please like, share, subscribe.