Skip to content

Orbital Elements

This example shows how to move between the three common orbit representations in ootk: TLEs, classical (Keplerian) orbital elements, and inertial state vectors. Use these conversions whenever you need to inspect an orbit's shape from tracking data, build a synthetic orbit from scratch, or export an orbit as a TLE for SGP4 consumers.

ts
import {
  ClassicalElements,
  Degrees,
  EpochUTC,
  J2000,
  Kilometers,
  KilometersPerSecond,
  Satellite,
  Tle,
  TleLine1,
  TleLine2,
  Vector3D,
} from 'ootk';

Run it

bash
npm run build
npx tsx ./examples/orbital-elements.ts

TLE to Elements

Propagate a Satellite (built from a TLE) to a specific date with toJ2000(), then call toClassicalElements() on the resulting state. Angle members on ClassicalElements are stored in radians, so the script converts to degrees for display. Derived quantities like period are available directly, and apogee/perigee altitudes fall out of the semi-major axis and eccentricity.

ts
// Example 1: Extract orbital elements from a TLE
console.log('=== Example 1: Extract Orbital Elements from TLE ===\n');

const issTle = 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,
});

const date = new Date('2024-01-28T13:05:27.451Z');
const j2000State = issTle.toJ2000(date);
const elements = j2000State.toClassicalElements();

console.log('ISS Orbital Elements:');
console.log(`  Semi-major axis: ${elements.semimajorAxis.toFixed(2)} km`);
console.log(`  Eccentricity: ${elements.eccentricity.toFixed(6)}`);
console.log(`  Inclination: ${(elements.inclination * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  Right Ascension: ${(elements.rightAscension * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  Arg of Perigee: ${(elements.argPerigee * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  True Anomaly: ${(elements.trueAnomaly * (180 / Math.PI)).toFixed(4)}°`);

// Calculate derived parameters
console.log(`\nDerived Parameters:`);
console.log(`  Period: ${elements.period.toFixed(2)} minutes`);
console.log(`  Apogee altitude: ${((elements.semimajorAxis * (1 + elements.eccentricity)) - 6378.137).toFixed(2)} km`);
console.log(`  Perigee altitude: ${((elements.semimajorAxis * (1 - elements.eccentricity)) - 6378.137).toFixed(2)} km`);

Elements to State Vector

ClassicalElements can also be constructed directly. The constructor accepts angles in degrees (they are converted internally), and toJ2000() produces the equivalent inertial position and velocity at the element epoch.

ts
// Example 2: Create classical elements and convert to state vector
console.log('\n=== Example 2: Create Orbital Elements and Convert to State Vector ===\n');

const customElements = new ClassicalElements({
  epoch: EpochUTC.fromDateTime(date),
  semimajorAxis: 8000 as Kilometers,
  eccentricity: 0.1,
  inclination: 45 as Degrees,
  rightAscension: 90 as Degrees,
  argPerigee: 30 as Degrees,
  trueAnomaly: 0 as Degrees,
});

console.log('Custom Orbit:');
console.log(`  Semi-major axis: ${customElements.semimajorAxis.toFixed(2)} km`);
console.log(`  Eccentricity: ${customElements.eccentricity.toFixed(6)}`);
console.log(`  Inclination: ${customElements.inclination.toFixed(4)}°`);
console.log(`  Period: ${customElements.period.toFixed(2)} minutes`);

const stateVector = customElements.toJ2000();

console.log(`\nState Vector:`);
console.log(`  Position: [${stateVector.position.x.toFixed(2)}, ${stateVector.position.y.toFixed(2)}, ${stateVector.position.z.toFixed(2)}] km`);
console.log(`  Velocity: [${stateVector.velocity.x.toFixed(6)}, ${stateVector.velocity.y.toFixed(6)}, ${stateVector.velocity.z.toFixed(6)}] km/s`);

State Vector to Elements

The reverse direction: build a J2000 state from raw position (km) and velocity (km/s) vectors, then recover the Keplerian elements with toClassicalElements(). This is the typical path when your input is a propagator output or a radar-derived state.

ts
// Example 3: Convert state vector to classical elements
console.log('\n=== Example 3: Create State Vector and Convert to Classical Elements ===\n');

const customState = new J2000(
  EpochUTC.fromDateTime(date),
  new Vector3D(
    -4040.9257 as Kilometers,
    -4884.0906 as Kilometers,
    3522.9643 as Kilometers,
  ),
  new Vector3D(
    5.4662 as KilometersPerSecond,
    -3.4425 as KilometersPerSecond,
    -2.4854 as KilometersPerSecond,
  ),
);

const derivedElements = customState.toClassicalElements();

console.log('Derived Orbital Elements:');
console.log(`  Semi-major axis: ${derivedElements.semimajorAxis.toFixed(2)} km`);
console.log(`  Eccentricity: ${derivedElements.eccentricity.toFixed(6)}`);
console.log(`  Inclination: ${(derivedElements.inclination * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  Right Ascension: ${(derivedElements.rightAscension * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  Arg of Perigee: ${(derivedElements.argPerigee * (180 / Math.PI)).toFixed(4)}°`);
console.log(`  True Anomaly: ${(derivedElements.trueAnomaly * (180 / Math.PI)).toFixed(4)}°`);

Elements to TLE

Tle.fromClassicalElements() fits a TLE to a set of elements. Note that TLEs encode SGP4 mean elements, not osculating elements, so reading the TLE back through SGP4 gives values close to (but not exactly) the input; the verification block shows the round trip lands within about 1 km of the GEO semi-major axis.

ts
// Example 4: Create TLE from classical elements
console.log('\n=== Example 4: Create TLE from Classical Elements ===\n');

const geoElements = new ClassicalElements({
  epoch: EpochUTC.fromDateTime(date),
  semimajorAxis: 42164 as Kilometers, // GEO altitude
  eccentricity: 0.0001,
  inclination: 0.1 as Degrees,
  rightAscension: 0 as Degrees,
  argPerigee: 0 as Degrees,
  trueAnomaly: 0 as Degrees,
});

const geoTle = Tle.fromClassicalElements(geoElements);

console.log('Generated TLE for GEO satellite:');
console.log(geoTle.line1);
console.log(geoTle.line2);

// Verify by reading back
const verifyTle = new Satellite({
  tle1: geoTle.line1,
  tle2: geoTle.line2,
});

const verifyState = verifyTle.toJ2000(date);
const verifyElements = verifyState.toClassicalElements();

console.log(`\nVerification - Semi-major axis: ${verifyElements.semimajorAxis.toFixed(2)} km`);
console.log(`Verification - Period: ${verifyElements.period.toFixed(2)} minutes (should be ~1436 min for GEO)`);

Orbit Types

The same ClassicalElements API describes any orbit regime. This block builds representative LEO, MEO, and Molniya-style HEO orbits and prints their apogee/perigee altitudes and periods.

ts
// Example 5: Different orbit types
console.log('\n=== Example 5: Different Orbit Types ===\n');

const orbits = [
  {
    name: 'LEO (Circular)',
    elements: new ClassicalElements({
      epoch: EpochUTC.fromDateTime(date),
      semimajorAxis: 6778 as Kilometers,
      eccentricity: 0.001,
      inclination: 51.6 as Degrees,
      rightAscension: 0 as Degrees,
      argPerigee: 0 as Degrees,
      trueAnomaly: 0 as Degrees,
    }),
  },
  {
    name: 'MEO (GPS-like)',
    elements: new ClassicalElements({
      epoch: EpochUTC.fromDateTime(date),
      semimajorAxis: 26560 as Kilometers,
      eccentricity: 0.01,
      inclination: 55 as Degrees,
      rightAscension: 0 as Degrees,
      argPerigee: 0 as Degrees,
      trueAnomaly: 0 as Degrees,
    }),
  },
  {
    name: 'HEO (Molniya)',
    elements: new ClassicalElements({
      epoch: EpochUTC.fromDateTime(date),
      semimajorAxis: 26554 as Kilometers,
      eccentricity: 0.74,
      inclination: 63.4 as Degrees,
      rightAscension: 0 as Degrees,
      argPerigee: 270 as Degrees,
      trueAnomaly: 0 as Degrees,
    }),
  },
];

orbits.forEach((orbit) => {
  console.log(`${orbit.name}:`);
  console.log(`  Altitude at apogee: ${((orbit.elements.semimajorAxis * (1 + orbit.elements.eccentricity)) - 6378.137).toFixed(2)} km`);
  console.log(`  Altitude at perigee: ${((orbit.elements.semimajorAxis * (1 - orbit.elements.eccentricity)) - 6378.137).toFixed(2)} km`);
  console.log(`  Period: ${orbit.elements.period.toFixed(2)} minutes`);
  console.log('');
});

Output

txt
=== Example 1: Extract Orbital Elements from TLE ===

ISS Orbital Elements:
  Semi-major axis: 6794.45 km
  Eccentricity: 0.001030
  Inclination: 51.5075°
  Right Ascension: 292.0110°
  Arg of Perigee: 125.3753°
  True Anomaly: 294.0422°

Derived Parameters:
  Period: 92.89 minutes
  Apogee altitude: 423.31 km
  Perigee altitude: 409.31 km

=== Example 2: Create Orbital Elements and Convert to State Vector ===

Custom Orbit:
  Semi-major axis: 8000.00 km
  Eccentricity: 0.100000
  Inclination: 45.0000°
  Period: 118.68 minutes

State Vector:
  Position: [2843.28, 2667.36, -6053.18] km
  Velocity: [-4.020084, 6.609623, 1.024256] km/s

=== Example 3: Create State Vector and Convert to Classical Elements ===

Derived Orbital Elements:
  Semi-major axis: 6427.20 km
  Eccentricity: 0.305487
  Inclination: 32.5870°
  Right Ascension: 110.7914°
  Arg of Perigee: 245.6083°
  True Anomaly: 229.9747°

=== Example 4: Create TLE from Classical Elements ===

Generated TLE for GEO satellite:
1 00001U 58001A   24028.54545661  .00000000  00000+0  00000+0 0  9995
2 00001   5.7296   0.0000 0001000   0.0000   0.0000 01.00274396    05

Verification - Semi-major axis: 42165.10 km
Verification - Period: 1436.12 minutes (should be ~1436 min for GEO)

=== Example 5: Different Orbit Types ===

LEO (Circular):
  Altitude at apogee: 406.64 km
  Altitude at perigee: 393.09 km
  Period: 92.56 minutes

MEO (GPS-like):
  Altitude at apogee: 20447.46 km
  Altitude at perigee: 19916.26 km
  Period: 717.96 minutes

HEO (Molniya):
  Altitude at apogee: 39825.82 km
  Altitude at perigee: 525.90 km
  Period: 717.72 minutes

Released under the AGPL-3.0 License.