DEV Community

SEN LLC
SEN LLC

Posted on

The Three Conditions for Seeing the ISS — A Browser-Only Visibility Calculator with Spherical Trigonometry

When ISS passes overhead at the right moment, you can step outside and watch a bright point of light streak from west to east at ~7 km/s — apparent magnitude around −5, brighter than Venus, no telescope needed. The interesting part of building a "is it visible right now?" tool is that just plotting the dot on a map doesn't answer the question. Three independent conditions all have to be true:

  1. ISS above your horizon (geometry)
  2. ISS sunlit (not in Earth's shadow)
  3. Your sky dark enough (sun far enough below your horizon)

All three fall out of one REST API call plus ~200 lines of spherical trig. Here's the math.

iss-tracker UI: equirectangular SVG world map with the live ISS position (red dot), the 2222 km footprint shown as a dashed red circle, the observer (blue dot, Tokyo) and the sun's subpoint (yellow dot). Below: status panels for ISS state, observer geometry, and the three-condition visibility checklist with verdict. Dark theme.

🛰️ Demo: https://sen.ltd/portfolio/iss-tracker/
📦 GitHub: https://github.com/sen-ltd/iss-tracker

The API: wheretheiss.at gives you everything you need in one shot

There are several ISS position APIs. The one that's HTTPS-friendly, no-auth, and returns all the fields the visibility math needs is api.wheretheiss.at:

$ curl -s https://api.wheretheiss.at/v1/satellites/25544 | jq
{
  "name": "iss",
  "id": 25544,
  "latitude": 28.857,
  "longitude": -6.671,
  "altitude": 416.42,            # km
  "velocity": 27603.81,          # km/h
  "visibility": "eclipsed",      # ← is the satellite sunlit or in shadow?
  "footprint": 4489.21,          # km
  "solar_lat": 15.30,            # ← sub-solar point (where the sun is overhead)
  "solar_lon": 197.78,
  "timestamp": 1777675555
}
Enter fullscreen mode Exit fullscreen mode

Two fields are gold:

  • visibility is a satellite-side property — the API does the Earth-shadow geometry for you. "daylight" means ISS is in sunlight; "eclipsed" means it's in Earth's shadow. That's condition 2 done.
  • solar_lat, solar_lon is the sub-solar point — the (lat, lon) where the sun is directly overhead at this instant. With the observer's lat/lon, the sun's altitude above the observer's horizon is one trig formula away. That's the input for condition 3.

The older api.open-notify.org is HTTPS-broken and doesn't even return altitude. Use wheretheiss.

Condition 1: ISS above the observer's horizon

Treat Earth as a sphere of radius R. Given satellite altitude h and the great-circle distance d from observer to ISS sub-point, the slant range and elevation come from the OES triangle (Observer, Earth-centre, Satellite) via the law of cosines:

slant² = R² + (R+h)² − 2R(R+h)·cos(d/R)
sin(elevation) = ((R+h)·cos(d/R) − R) / slant
Enter fullscreen mode Exit fullscreen mode

elevation > 0 means ISS is above the observer's horizon — physically possible to see. Negative means it's on the far side of the planet, no chance.

export function elevationDeg(observerLat, observerLon, subLat, subLon, altitudeKm) {
  const d = haversineKm(observerLat, observerLon, subLat, subLon);
  const θ = d / EARTH_RADIUS_KM;
  const R = EARTH_RADIUS_KM, h = altitudeKm;
  const slantSq = R*R + (R+h)**2 - 2*R*(R+h)*Math.cos(θ);
  const sinE = ((R+h)*Math.cos(θ) - R) / Math.sqrt(slantSq);
  return Math.asin(Math.max(-1, Math.min(1, sinE))) * 180 / Math.PI;
}
Enter fullscreen mode Exit fullscreen mode

The footprint radius drops out

The locus of points where elevation = 0 (line-of-sight tangent to the Earth at the observer's feet) is a small circle on the sphere — the satellite's "footprint." Geometry:

cos α = R / (R + h)
footprint_km = R · α
Enter fullscreen mode Exit fullscreen mode

For ISS at 408 km, α ≈ 20° and the footprint radius is ~2222 km. Anyone outside that radius from the sub-point is below the horizon, full stop.

export function footprintKm(altitudeKm) {
  const cosAlpha = EARTH_RADIUS_KM / (EARTH_RADIUS_KM + altitudeKm);
  return EARTH_RADIUS_KM * Math.acos(cosAlpha);
}
Enter fullscreen mode Exit fullscreen mode

Condition 2: ISS in sunlight, not Earth's shadow

Even if ISS is directly overhead at midnight, if it's in Earth's shadow it's invisible. It has no light source. ISS reflects sunlight off its solar panels — that's why it's bright when sunlit.

ISS orbits Earth every ~90 minutes, so each orbit it passes through Earth's shadow for some fraction of the loop. That "ISS overhead but eclipsed" case is exactly why a midnight pass over your head can fail to be visible.

The API handles this for you: iss.visibility === "daylight" ⇒ sunlit ⇒ condition 2 met.

Condition 3: the observer's sky is dark enough

Daytime ISS isn't visible — the bright sky drowns out a 5th-magnitude moving point. We need the observer to be in twilight or darker.

Sun altitude from sub-solar point is one formula:

sin(sun_altitude) = sin(obs_lat) · sin(sun_lat)
                  + cos(obs_lat) · cos(sun_lat) · cos(obs_lon − sun_lon)
Enter fullscreen mode Exit fullscreen mode

Astronomical convention:

Sun altitude Phase ISS visible?
> 0° Day No
0° to −6° Civil twilight Sky still too bright
−6° to −12° Nautical twilight Marginal
−12° to −18° Astronomical twilight Yes
< −18° Full night Best

The practical threshold for ISS visibility is sun altitude < −6° (civil twilight or darker).

export function sunAltitudeDeg(observerLat, observerLon, sunLat, sunLon) {
  const φo = deg2rad(observerLat);
  const φs = deg2rad(sunLat);
  const  = deg2rad(observerLon - sunLon);
  const sinH = Math.sin(φo)*Math.sin(φs) + Math.cos(φo)*Math.cos(φs)*Math.cos();
  return rad2deg(Math.asin(Math.max(-1, Math.min(1, sinH))));
}
Enter fullscreen mode Exit fullscreen mode

The combined verdict:

const visible = elevation > 0
             && iss.visibility === "daylight"
             && sunAltitudeDeg(obs.lat, obs.lon, iss.solar_lat, iss.solar_lon) < -6;
Enter fullscreen mode Exit fullscreen mode

What this tool deliberately doesn't do

heavens-above.com and NASA's spotthestation tell you "ISS will pass over your location at 19:42 UTC tomorrow, max elevation 67°, visible from 19:43 to 19:47." To do that yourself you need:

  1. Two-Line Element sets (TLEs) from celestrak.com
  2. SGP4 orbital propagator — the standard NORAD model for satellites in low Earth orbit, ~2000 lines of dense numerical code (atmospheric drag, J2/J3/J4 perturbations, geopotential)
  3. A sweep over the next 24-48 hours sampling at minute resolution
  4. Apply the three-condition visibility check at each sample to find the windows

satellite.js is ~50 KB of bundled SGP4 if you want it. But spotthestation already does this perfectly, so this tool stays focused on right now: 200 lines, no library, no orbital propagator.

Map rendering — no coastlines, on purpose

The expected ISS-tracker visual is "moving icon on a Mercator world map." Shipping coastline data costs ~50-100 KB of bundled GeoJSON and brings projection-distortion problems near the poles. Equirectangular x = lon, y = -lat is one line of code; a graticule (lat/lon grid) gives the eye anchor points; the data overlay (ISS dot + footprint cap + observer + sub-solar) carries the visual weight. Cleaner, no dependency, fits the "the math is the show" angle.

<svg viewBox="-180 -90 360 180">
  <g id="graticule"></g>
  <g id="footprint"></g>
  <g id="iss"></g>
  <g id="observer"></g>
  <g id="sun"></g>
</svg>
Enter fullscreen mode Exit fullscreen mode

Takeaways

  • ISS visibility = AND of three independent conditions: above-horizon (geometry), sunlit (API field), observer-sky-dark (sun altitude < −6°).
  • api.wheretheiss.at is the right API — HTTPS, no auth, returns altitude / velocity / visibility / sub-solar point in one call.
  • The geometry: haversine distance, OES triangle for elevation, cos α = R/(R+h) for footprint, sin·sin + cos·cos·cos for sun altitude.
  • Pass prediction (next time ISS will be visible) deliberately out of scope — that needs TLEs and an SGP4 propagator. NASA's spotthestation already nails it.

Full source on GitHubiss.js is the 200-line geometry module, tests/iss.test.js is 24 cases. MIT.

Live demo updates every 5 s. The "Use geolocation" button pre-fills the observer to your current location.

Top comments (2)

Collapse
 
griffinmonkey43 profile image
g310siviglia@gmail.com

Very cool👍️. Are you using Github to host your demo?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.