See A Satellite Tonight is my most successful side project so far, with 5 million users since launch in 2019. It’s been entirely a solo project, and I’ve used it as a proving ground for a bunch of tech that I wanted to learn. It took dozens of APIs and services to put together the experience I wanted:
In this post I’ll describe how it all works. Frontend tech, satellite-related calculations, backend tech, plus a little about my motivations for building the site.
Frontend tech
See A Satellite Tonight is a very client-heavy site. It’s a single-page application that is written mostly as a single file, HTML and JavaScript and CSS all together. If I was collaborating on the site with others I might try to separate things out more, but I really like the option to keep everything in a single file for solo development. I miss the simplicity when I develop for other platforms.
I wrote the site with vanilla JavaScript, no frameworks and no build process. It’s not obfuscated, so you can see the code in the web inspector, though it wasn’t structured to be readable by others 😀
Satellite calculations
The core task that See A Satellite Tonight performs is calculating exactly when and where a satellite will be visible to you. How does this calculation work?
First, I need to know where you are. The browser’s Geolocation API is used for that. Of course I have to ask permission to use it, and the user may deny it. This is tricky because I can’t display anything useful without asking for geolocation first, and the user may not understand why it’s necessary, or be suspicious of data collection. I only use the geolocation data client-side and don’t send it to the server, but the user has no way of really verifying that. If the user denies geolocation permission, I fall back to IP geolocation. But this isn’t precise enough for the site’s best feature, Street View, to work. So in that case I replace the Street View display with a search box inviting the user to enter their street address, backed by Google Maps’s geocoding API, and carefully constructed for browser autofill to work. Autofill likes to split the user’s address into many different fields like street number, postal code etc, which is not at all necessary. So I have a bunch of hidden form fields for autofill to use, and when they get filled I move that data back into the main search field.
Now that I have the user’s location, I need to figure out where the satellites are. At page load, the first thing that happens is a request for satellite ephemeris (orbit) data from /satellites/brightest-sats.txt. This request returns Two-line elements (TLEs) for every satellite that is potentially bright enough to be visible to the human eye (currently 774 satellites as I write this). This data comes from the US Space Force 18th Space Defense Squadron for most satellites, and Celestrak for Starlink. The TLEs describe an orbit for each satellite, but we’re still far away from what we want: a list of times that satellites will be visible.
Now it’s time for some math. First I need to calculate the position of each satellite over time. The process of turning a TLE into a position at a specific time is called “orbit propagation”, and it is done using a standard piece of code called SGP4. I believe SGP4 was originally written in Fortran in the 1960s. It’s been ported to many different languages, including JavaScript, but I decided to use the C++ version both for speed and accuracy. The C++ version is likely what is used when the TLEs are originally generated, and numeric code like this can often have subtle rounding issues after porting to other languages, so it’s best to use the same implementation if possible. Luckily Emscripten makes it relatively easy to reuse C++ code on the web.
Using the C++ SGP4 propagator running in WASM, for each TLE I calculate the satellite’s position (in ECEF coordinates) every minute for the next 5 days (7200 minutes). I spread the calculation over a few Web Worker threads to avoid hanging the UI and take advantage of multicore processors. Now for each minute we want to calculate whether the satellite will be visible. This is a complex calculation which requires a lot of simplifying approximations. Here are the factors we need to consider:
- Is the satellite position more than a couple of degrees above the horizon at the user’s location? This is a straightforward geometric calculation, given ECEF coordinates for the user and satellite, and the Earth’s surface normal at the user’s location (which is not just the normalized ECEF coordinate because the Earth is not exactly a sphere).
- Is the satellite lit by the sun, or in Earth’s shadow? Satellites, like the Moon, don’t emit their own light, and can only be seen when they reflect sunlight. Unlike the Moon, low orbiting satellites are in Earth’s shadow almost half the time, so this calculation is very important.
- How far away is the satellite from the user? Brightness decreases according to the inverse square law.
- How much sunlight is the satellite reflecting in the user’s direction? In reality this depends on the exact size and shape and orientation of the satellite. The size is easy to model, but the shape and orientation are difficult or even unknown. So we can take the satellite’s size, assume it’s a sphere, and calculate its “phase” as if it was the Moon. This is not a great approximation, and someday maybe I can do better with specific knowledge of the shape and orientation of different satellites. This is a geometric calculation using the positions and relative angles of the satellite, user, and Sun. Here’s some information about how it’s done.
- How bright is the sky above the user? If the sky is brighter than the satellite, then the user won’t see it. There are a few sub-factors to consider:
- Is the Sun above or below the horizon? If below, how far below? I use the Sun position calculated by the excellent Cesium library, and visibility tables from this paper.
- Is the user near a city with bright lights? (I’m not calculating this yet, but there is some data available about sky brightness due to city lights, so it’s probably possible to do.)
- Is the Moon out? How bright is it? How close is the satellite to the Moon? (I don’t actually calculate this yet)
- How much of the satellite’s light is blocked by the atmosphere? This depends on the height of the satellite above the horizon. The closer to the horizon, the longer the light has to travel inside the atmosphere, and the more will be blocked. It also depends on atmospheric conditions including clouds and smog and even the seasons, but I’m not currently accounting for those. I’m using some tables from this paper for this calculation.
Whew! After all that is done, I apply a brightness threshold to calculate a binary “is visible” value for each satellite at each minute of the next 5 days. Then I can aggregate those into a list of time periods for each satellite. In one final step, if multiple satellites are visible at the same time I combine their time periods together into one larger period. This is mostly for Starlink since you can easily have 40+ satellites going over at around the same time, and showing 40+ separate viewing times would be excessive.
User interface
I wanted the site to appeal to people who have no idea about space or satellites at all. 3D graphics don’t just look flashy; they can give a good intuitive sense of what’s going on. I wanted people to appreciate that they’re really seeing satellites traveling in orbits above them, and not just abstract dots in the sky. I’m using the excellent Cesium library for the 3D globe visualization, and also for some of the satellite visibility calculations. Cesium is free, but they charge for use of high resolution globe imagery. I solved this problem by hosting low resolution globe imagery myself, and not allowing you to zoom in so you can’t tell it’s low resolution. Zooming in the 3D view wouldn’t really improve t