Skip to content

Satellite Passes

Predicting satellite visibility over a ground station, computing look angles (azimuth, elevation, range), and checking field-of-view constraints with the sensor module. Use this when scheduling contacts or determining whether a radar can actually observe a target, not just whether it is above the horizon.

ts
import {
  Degrees,
  GroundStation,
  Kilometers,
  PhasedArrayRadar,
  Satellite,
  SensorType,
  TleLine1,
  TleLine2,
} from 'ootk';

Run it

bash
npm run build
npx tsx ./examples/satellite-passes.ts

Create a ground station

GroundStation holds the observer's geodetic position, and Satellite wraps a TLE with SGP4 propagation. Orbital metadata like inclination and period is available directly on the satellite.

ts
console.log('=== Example 1: ISS Pass Prediction ===\n');

// Create a ground station
const groundStation = new GroundStation({
  lat: 41.754785 as Degrees,
  lon: -70.539151 as Degrees,
  alt: 0.060966 as Kilometers,
  name: 'Cape Cod',
});

// Create ISS satellite
const iss = new Satellite({
  tle1: '1 25544U 98067A   24028.54545847  .00031576  00000-0  57240-3 0  9991' as TleLine1,
  tle2: '2 25544  51.6418 292.2590 0002595 167.5319 252.0460 15.49326324436741' as TleLine2,
});

console.log(`Ground Station: ${groundStation.name}`);
console.log(`  Location: ${groundStation.lat}° N, ${Math.abs(groundStation.lon)}° W`);
console.log(`  Altitude: ${groundStation.alt} km`);

console.log('\nSatellite: ISS');
console.log(`  Inclination: ${iss.inclination}°`);
console.log(`  Period: ${iss.period} minutes`);

Visibility check

Satellite.rae(groundStation, date) returns range, azimuth, and elevation (or null if propagation fails). Elevation above 0° means the satellite is geometrically above the horizon.

ts
console.log('\n=== Example 2: Satellite Visibility Check ===\n');

const checkTime = new Date('2024-01-28T12:00:00.000Z');

// Get current look angles
const rae = iss.rae(groundStation, checkTime);

if (rae) {
  console.log(`Time: ${checkTime.toISOString()}`);
  console.log('\nLook Angles:');
  console.log(`  Azimuth:   ${rae.az.toFixed(2)}°`);
  console.log(`  Elevation: ${rae.el.toFixed(2)}°`);
  console.log(`  Range:     ${rae.rng.toFixed(2)} km`);

  // Check if satellite is visible
  const isVisible = rae.el > 0;

  console.log(`\nSatellite is ${isVisible ? 'VISIBLE' : 'BELOW HORIZON'}`);

  if (isVisible) {
    console.log(`\nDirection: ${getCardinalDirection(rae.az)}`);
    console.log(`Elevation: ${getElevationDescription(rae.el)}`);
  }
}

Field of view constraints

A PhasedArrayRadar attaches to the ground station via addSensor() plus setParent(), and its FieldOfView defines a cone (halfAngle around a boresight) plus minRange/maxRange and minElevation. canObserve(sat, date) applies all constraints at once; getRae() exposes the raw geometry so you can report which specific constraint failed.

ts
console.log('\n=== Example 3: Field of View Constraints ===\n');

// Create a phased array radar and attach it to the ground station.
// The field of view is a cone around the boresight, plus range bounds and
// a minimum elevation.
const radar = new PhasedArrayRadar({
  id: 1,
  name: 'Cape Cod Radar',
  sensorType: SensorType.PHASED_ARRAY_RADAR,
  beamwidth: 2 as Degrees,
  boresightAz: [0 as Degrees],
  boresightEl: [45 as Degrees],
  fieldOfView: {
    halfAngle: 45 as Degrees,
    minRange: 100 as Kilometers,
    maxRange: 5556 as Kilometers,
    minElevation: 10 as Degrees,
  },
});

// Attach radar to ground station
groundStation.addSensor(radar);
radar.setParent(groundStation);

const fov = radar.fieldOfView;

console.log(`Sensor: ${radar.name}`);
console.log('Field of View Constraints:');
console.log(`  Boresight:     Az ${radar.boresightAz[0]}°, El ${radar.boresightEl[0]}°`);
console.log(`  Cone Half-Angle: ${fov.halfAngle}°`);
console.log(`  Min Elevation:   ${fov.minElevation}°`);
console.log(`  Range:           ${fov.minRange} - ${fov.maxRange} km`);

// Check if satellite is in FOV
const inFov = radar.canObserve(iss, checkTime);

console.log(`\nAt ${checkTime.toISOString()}:`);
console.log(`  Satellite in FOV: ${inFov ? 'YES' : 'NO'}`);

if (inFov) {
  console.log('  The satellite meets all FOV constraints');
} else {
  const raeCheck = radar.getRae(iss, checkTime);

  if (raeCheck) {
    console.log('  Constraints not met:');

    if (raeCheck.el < fov.minElevation) {
      console.log(`    - Elevation too low (${raeCheck.el.toFixed(1)}° < ${fov.minElevation}°)`);
    }

    if (raeCheck.rng < fov.minRange) {
      console.log(`    - Range too close (${raeCheck.rng.toFixed(0)} km < ${fov.minRange} km)`);
    }

    if (raeCheck.rng > fov.maxRange) {
      console.log(`    - Range too far (${raeCheck.rng.toFixed(0)} km > ${fov.maxRange} km)`);
    }
  }
}

Tracking over time

Sampling rae() on a fixed interval distinguishes three states: below horizon, visible (above horizon but outside sensor constraints), and inside the radar's FOV.

ts
console.log('\n=== Example 4: Satellite Tracking Over Time ===\n');

const trackStart = new Date('2024-01-28T12:00:00.000Z');

console.log('ISS Position every 5 minutes:\n');
console.log('Time                      Az      El     Range   Status');
console.log('─────────────────────  ──────  ──────  ───────  ────────');

for (let i = 0; i < 12; i++) {
  const trackTime = new Date(trackStart.getTime() + i * 5 * 60 * 1000);
  const trackRae = iss.rae(groundStation, trackTime);

  if (trackRae) {
    const timeStr = trackTime.toISOString().substring(11, 19);
    const azStr = trackRae.az.toFixed(1).padStart(6);
    const elStr = trackRae.el.toFixed(1).padStart(6);
    const rngStr = trackRae.rng.toFixed(0).padStart(7);

    let status = 'Below horizon';

    if (trackRae.el > 0) {
      status = 'Visible';

      if (radar.canObserve(iss, trackTime)) {
        status = 'In FOV';
      }
    }

    console.log(`${timeStr}          ${azStr}° ${elStr}° ${rngStr} km  ${status}`);
  }
}

Multiple satellites

The same station and sensor can evaluate any number of targets; each satellite carries its own propagator, so the per-target cost is just an SGP4 call.

ts
console.log('\n=== Example 5: Tracking Multiple Satellites ===\n');

const satellites = [
  {
    name: 'ISS',
    sat: new Satellite({
      tle1: '1 25544U 98067A   24028.54545847  .00031576  00000-0  57240-3 0  9991' as TleLine1,
      tle2: '2 25544  51.6418 292.2590 0002595 167.5319 252.0460 15.49326324436741' as TleLine2,
    }),
  },
  {
    name: 'HST (Hubble)',
    sat: new Satellite({
      tle1: '1 20580U 90037B   24028.50123227  .00000825  00000-0  39644-4 0  9997' as TleLine1,
      tle2: '2 20580  28.4696 273.2640 0002975 297.7865 189.2151 15.09696656316758' as TleLine2,
    }),
  },
];

const multiCheckTime = new Date('2024-01-28T18:00:00.000Z');

console.log(`Time: ${multiCheckTime.toISOString()}\n`);
console.log('Satellite      Az      El     Range    Visible   In FOV');
console.log('────────────  ──────  ──────  ───────  ────────  ──────');

satellites.forEach((satInfo) => {
  const satRae = satInfo.sat.rae(groundStation, multiCheckTime);

  if (satRae) {
    const satVisible = satRae.el > 0;
    const satInFov = radar.canObserve(satInfo.sat, multiCheckTime);

    const nameStr = satInfo.name.padEnd(12);
    const azStr = satRae.az.toFixed(1).padStart(6);
    const elStr = satRae.el.toFixed(1).padStart(6);
    const rngStr = satRae.rng.toFixed(0).padStart(7);
    const visStr = (satVisible ? 'Yes' : 'No').padEnd(8);
    const fovStr = satInFov ? 'Yes' : 'No';

    console.log(`${nameStr}  ${azStr}° ${elStr}° ${rngStr} km  ${visStr}  ${fovStr}`);
  }
});

Helpers

ts
function getCardinalDirection(azimuth: number): string {
  const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
  const index = Math.round(azimuth / 22.5) % 16;

  return directions[index];
}

function getElevationDescription(elevation: number): string {
  if (elevation > 80) {
    return 'Nearly overhead';
  }
  if (elevation > 60) {
    return 'Very high in the sky';
  }
  if (elevation > 45) {
    return 'High in the sky';
  }
  if (elevation > 30) {
    return 'Medium elevation';
  }
  if (elevation > 15) {
    return 'Low in the sky';
  }

  return 'Near the horizon';
}

Output

txt
=== Example 1: ISS Pass Prediction ===

Ground Station: Cape Cod
  Location: 41.754785° N, 70.539151° W
  Altitude: 0.060966 km

Satellite: ISS
  Inclination: 51.6418°
  Period: 92.94362186284005 minutes

=== Example 2: Satellite Visibility Check ===

Time: 2024-01-28T12:00:00.000Z

Look Angles:
  Azimuth:   309.51°
  Elevation: -54.65°
  Range:     10918.10 km

Satellite is BELOW HORIZON

=== Example 3: Field of View Constraints ===

Sensor: Cape Cod Radar
Field of View Constraints:
  Boresight:     Az 0°, El 45°
  Cone Half-Angle: 45°
  Min Elevation:   10°
  Range:           100 - 5556 km

At 2024-01-28T12:00:00.000Z:
  Satellite in FOV: NO
  Constraints not met:
    - Elevation too low (-54.7° < 10°)
    - Range too far (10918 km > 5556 km)

=== Example 4: Satellite Tracking Over Time ===

ISS Position every 5 minutes:

Time                      Az      El     Range   Status
─────────────────────  ──────  ──────  ───────  ────────
12:00:00           309.5°  -54.7°   10918 km  Below horizon
12:05:00           289.7°  -57.0°   11196 km  Below horizon
12:10:00           268.8°  -57.9°   11286 km  Below horizon
12:15:00           248.0°  -56.9°   11170 km  Below horizon
12:20:00           228.5°  -54.2°   10845 km  Below horizon
12:25:00           210.7°  -50.2°   10319 km  Below horizon
12:30:00           194.5°  -45.3°    9617 km  Below horizon
12:35:00           178.9°  -39.8°    8779 km  Below horizon
12:40:00           163.0°  -34.3°    7872 km  Below horizon
12:45:00           145.6°  -29.2°    6995 km  Below horizon
12:50:00           125.8°  -25.1°    6292 km  Below horizon
12:55:00           103.5°  -23.1°    5930 km  Below horizon

=== Example 5: Tracking Multiple Satellites ===

Time: 2024-01-28T18:00:00.000Z

Satellite      Az      El     Range    Visible   In FOV
────────────  ──────  ──────  ───────  ────────  ──────
ISS             51.8°  -31.6°    7440 km  No        No
HST (Hubble)   217.8°   -4.3°    3188 km  No        No

Released under the AGPL-3.0 License.