Claude Code Plugins

Community-maintained marketplace

Feedback

Calculate ISS orbital positions, propagate TLEs, and validate satellite tracking accuracy. Use when working with satellite.js, TLE data, orbital mechanics, position calculations, or SGP4 propagation.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name iss-orbit-calculation
description Calculate ISS orbital positions, propagate TLEs, and validate satellite tracking accuracy. Use when working with satellite.js, TLE data, orbital mechanics, position calculations, or SGP4 propagation.
allowed-tools Read, Bash, Write, Grep

ISS Orbit Calculation Skill

This skill provides comprehensive capabilities for satellite orbital calculations, TLE propagation, and position validation specific to the International Space Station.

When to Use This Skill

Automatically invoke this skill when:

  • Implementing satellite.js integration
  • Working with Two-Line Element (TLE) data
  • Calculating ISS positions from orbital parameters
  • Converting between coordinate systems (ECI, geodetic)
  • Validating orbital calculations
  • Implementing pass predictions
  • Debugging position accuracy issues

Core Competencies

1. TLE Data Management

TLE Format Understanding:

Line 0: ISS (ZARYA)
Line 1: 1 25544U 98067A   24001.50000000  .00016717  00000-0  10270-3 0  9005
Line 2: 2 25544  51.6400 208.9163 0006317  69.9862  25.2906 15.54225995 00001

Line 1 breakdown:

  • Position 1: Line number (1)
  • Position 3-7: Satellite catalog number (25544 for ISS)
  • Position 10-17: Epoch year and day
  • Position 34-43: First derivative of mean motion
  • Position 54-61: Drag term
  • Position 63-68: Element set number
  • Position 69: Checksum

Line 2 breakdown:

  • Position 1: Line number (2)
  • Position 3-7: Satellite catalog number
  • Position 9-16: Inclination (degrees)
  • Position 18-25: Right ascension of ascending node (degrees)
  • Position 27-33: Eccentricity
  • Position 35-42: Argument of perigee (degrees)
  • Position 44-51: Mean anomaly (degrees)
  • Position 53-63: Mean motion (revolutions per day)
  • Position 64-68: Revolution number at epoch
  • Position 69: Checksum

2. Implementation Pattern

import * as satellite from 'satellite.js';

class ISSOrbitCalculator {
  private satrec: satellite.SatRec | null = null;
  private tleEpoch: Date | null = null;
  
  /**
   * Initialize with TLE data
   * @param line1 - TLE line 1
   * @param line2 - TLE line 2
   */
  initializeTLE(line1: string, line2: string): void {
    // Validate TLE format
    if (!this.validateTLEFormat(line1, line2)) {
      throw new Error('Invalid TLE format');
    }
    
    // Parse TLE
    this.satrec = satellite.twoline2satrec(line1, line2);
    
    // Check for parsing errors
    if (this.satrec.error) {
      throw new Error(`TLE parsing error: ${this.satrec.error}`);
    }
    
    // Extract epoch
    this.tleEpoch = this.extractEpoch(line1);
    
    console.log('TLE initialized:', {
      epoch: this.tleEpoch.toISOString(),
      inclination: this.satrec.inclo * (180 / Math.PI),
      eccentricity: this.satrec.ecco
    });
  }
  
  /**
   * Calculate position at current time
   */
  getCurrentPosition(): ISSPosition {
    return this.getPositionAt(new Date());
  }
  
  /**
   * Calculate position at specific time
   */
  getPositionAt(date: Date): ISSPosition {
    if (!this.satrec) {
      throw new Error('TLE not initialized');
    }
    
    // Propagate orbit to specified time
    const positionAndVelocity = satellite.propagate(this.satrec, date);
    
    // Check for propagation errors
    if ((positionAndVelocity.position as any) === false) {
      throw new Error('Propagation error - position calculation failed');
    }
    
    const positionEci = positionAndVelocity.position as satellite.EciVec3<number>;
    const velocityEci = positionAndVelocity.velocity as satellite.EciVec3<number>;
    
    // Convert ECI to geodetic coordinates
    const gmst = satellite.gstime(date);
    const positionGd = satellite.eciToGeodetic(positionEci, gmst);
    
    // Calculate velocity magnitude
    const velocity = Math.sqrt(
      velocityEci.x ** 2 + velocityEci.y ** 2 + velocityEci.z ** 2
    );
    
    const position: ISSPosition = {
      latitude: satellite.degreesLat(positionGd.latitude),
      longitude: satellite.degreesLong(positionGd.longitude),
      altitude: positionGd.height, // km
      velocity: velocity, // km/s
      timestamp: date.getTime(),
      visibility: this.calculateVisibility(positionEci, date)
    };
    
    // Validate position
    this.validatePosition(position);
    
    return position;
  }
  
  /**
   * Generate orbital path
   */
  generatePath(startTime: Date, durationMinutes: number, stepSeconds: number = 10): ISSPosition[] {
    const path: ISSPosition[] = [];
    const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
    
    for (let time = startTime.getTime(); time <= endTime.getTime(); time += stepSeconds * 1000) {
      const position = this.getPositionAt(new Date(time));
      path.push(position);
    }
    
    return path;
  }
  
  private validateTLEFormat(line1: string, line2: string): boolean {
    // Check line lengths
    if (line1.length !== 69 || line2.length !== 69) {
      return false;
    }
    
    // Check line numbers
    if (line1[0] !== '1' || line2[0] !== '2') {
      return false;
    }
    
    // Verify checksums
    return this.verifyChecksum(line1) && this.verifyChecksum(line2);
  }
  
  private verifyChecksum(line: string): boolean {
    const data = line.substring(0, 68);
    const checksum = parseInt(line[68]);
    
    let sum = 0;
    for (const char of data) {
      if (char >= '0' && char <= '9') {
        sum += parseInt(char);
      } else if (char === '-') {
        sum += 1;
      }
    }
    
    return (sum % 10) === checksum;
  }
  
  private extractEpoch(line1: string): Date {
    const yearDay = line1.substring(18, 32);
    const year = parseInt(yearDay.substring(0, 2));
    const fullYear = year < 57 ? 2000 + year : 1900 + year;
    const dayOfYear = parseFloat(yearDay.substring(2));
    
    const epoch = new Date(fullYear, 0, 1);
    epoch.setDate(dayOfYear);
    
    return epoch;
  }
  
  private validatePosition(position: ISSPosition): void {
    // ISS altitude range: 370-460 km typically
    if (position.altitude < 300 || position.altitude > 500) {
      console.warn(`Altitude outside typical range: ${position.altitude.toFixed(2)} km`);
    }
    
    // ISS velocity: ~7.66 km/s
    if (position.velocity < 7.4 || position.velocity > 7.9) {
      console.warn(`Velocity outside typical range: ${position.velocity.toFixed(3)} km/s`);
    }
    
    // Coordinate bounds
    if (position.latitude < -90 || position.latitude > 90) {
      throw new Error(`Invalid latitude: ${position.latitude}`);
    }
    
    if (position.longitude < -180 || position.longitude > 180) {
      throw new Error(`Invalid longitude: ${position.longitude}`);
    }
  }
  
  private calculateVisibility(positionEci: satellite.EciVec3<number>, date: Date): 'daylight' | 'eclipse' {
    // Simplified visibility calculation
    // ISS is in daylight if sun-ISS vector and earth-ISS vector
    // have positive dot product
    
    const sunPosition = this.getSunPositionECI(date);
    
    // Vector from ISS to Sun
    const toSun = {
      x: sunPosition.x - positionEci.x,
      y: sunPosition.y - positionEci.y,
      z: sunPosition.z - positionEci.z
    };
    
    // Dot product with position vector
    const dotProduct = 
      toSun.x * positionEci.x +
      toSun.y * positionEci.y +
      toSun.z * positionEci.z;
    
    return dotProduct > 0 ? 'daylight' : 'eclipse';
  }
  
  private getSunPositionECI(date: Date): satellite.EciVec3<number> {
    // Simplified sun position (good enough for ISS visibility)
    const T = (this.julianDate(date) - 2451545.0) / 36525.0;
    const L = 280.460 + 36000.771 * T; // Mean longitude
    const M = 357.5291092 + 35999.05034 * T; // Mean anomaly
    
    const lambda = (L + 1.915 * Math.sin(M * Math.PI / 180)) * Math.PI / 180;
    const epsilon = 23.439 * Math.PI / 180; // Obliquity
    
    const AU = 149597870.7; // km
    
    return {
      x: AU * Math.cos(lambda),
      y: AU * Math.cos(epsilon) * Math.sin(lambda),
      z: AU * Math.sin(epsilon) * Math.sin(lambda)
    };
  }
  
  private julianDate(date: Date): number {
    return (date.getTime() / 86400000.0) + 2440587.5;
  }
  
  /**
   * Check TLE age (should refresh if > 12 hours old)
   */
  getTLEAge(): number {
    if (!this.tleEpoch) return Infinity;
    return (Date.now() - this.tleEpoch.getTime()) / (1000 * 60 * 60);
  }
}

3. Coordinate System Conversions

ECI (Earth-Centered Inertial) to Geodetic:

  • ECI: Fixed coordinate system with origin at Earth's center
  • Geodetic: Latitude, longitude, altitude on Earth's surface
  • Requires GMST (Greenwich Mean Sidereal Time) for conversion
  • GMST accounts for Earth's rotation
// Example conversion
const gmst = satellite.gstime(new Date());
const positionGd = satellite.eciToGeodetic(positionEci, gmst);

const latitude = satellite.degreesLat(positionGd.latitude);
const longitude = satellite.degreesLong(positionGd.longitude);
const altitude = positionGd.height; // km

4. Accuracy Validation

Always validate calculated positions against known data:

async function validateAccuracy(calculated: ISSPosition): Promise<number> {
  // Fetch reference position from API
  const response = await fetch('https://api.wheretheiss.at/v1/satellites/25544');
  const reference = await response.json();
  
  // Calculate distance error using Haversine formula
  const R = 6371; // Earth radius (km)
  const dLat = (reference.latitude - calculated.latitude) * Math.PI / 180;
  const dLon = (reference.longitude - calculated.longitude) * Math.PI / 180;
  
  const a = 
    Math.sin(dLat / 2) ** 2 +
    Math.cos(calculated.latitude * Math.PI / 180) *
    Math.cos(reference.latitude * Math.PI / 180) *
    Math.sin(dLon / 2) ** 2;
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distance = R * c;
  
  console.log('Position accuracy:', {
    calculated: [calculated.latitude, calculated.longitude],
    reference: [reference.latitude, reference.longitude],
    error: distance.toFixed(2) + ' km'
  });
  
  // Error should be < 50km for good TLE
  return distance;
}

Technical Standards

TLE Freshness

  • Optimal: < 3 days old
  • Acceptable: < 7 days old
  • Warning: 7-14 days old
  • Critical: > 14 days old

ISS Orbital Parameters

  • Altitude: 370-460 km (typically ~420 km)
  • Velocity: 7.4-7.9 km/s (typically ~7.66 km/s)
  • Inclination: ~51.6 degrees
  • Orbital period: ~90 minutes
  • Eccentricity: < 0.01 (nearly circular)

Position Validation Thresholds

  • Latitude: Must be [-90, 90]
  • Longitude: Must be [-180, 180]
  • Altitude: Warn if outside [300, 500] km
  • Velocity: Warn if outside [7.4, 7.9] km/s
  • Position error: < 50 km vs API is good

Common Issues and Solutions

Issue: Stale TLE Data

Symptom: Positions drift from reality Solution: Implement automatic TLE refresh every 12 hours

setInterval(async () => {
  const tle = await fetchFreshTLE();
  calculator.initializeTLE(tle.line1, tle.line2);
}, 12 * 60 * 60 * 1000);

Issue: Propagation Errors

Symptom: position returns false or NaN Solution: Always check error codes and handle gracefully

const result = satellite.propagate(satrec, date);
if ((result.position as any) === false) {
  // Fall back to API or use last known position
  console.error('Propagation failed, using fallback');
}

Issue: Coordinate Conversion Errors

Symptom: Longitude jumps or invalid values Solution: Ensure GMST is calculated correctly

// Always use satellite.gstime, not custom calculation
const gmst = satellite.gstime(date);
const positionGd = satellite.eciToGeodetic(positionEci, gmst);

Issue: Performance on Mobile

Symptom: Calculations slow on mobile devices Solution: Throttle calculations and cache results

// Calculate once per second, interpolate for 60 FPS
setInterval(() => {
  const position = calculator.getCurrentPosition();
  positionCache = position;
}, 1000);

// Use cached position for rendering
requestAnimationFrame(() => {
  renderPosition(positionCache);
});

Testing Requirements

Every orbital calculation implementation should include:

  1. TLE validation tests: Format, checksum, freshness
  2. Position calculation tests: Valid coordinates, ranges
  3. Accuracy validation tests: Compare against API
  4. Edge case tests: Date boundaries, orbit wrapping
  5. Performance tests: Calculation time < 10ms
  6. Error handling tests: Invalid TLE, propagation failures

Integration Checklist

  • TLE fetching from wheretheiss.at API
  • TLE validation (format, checksum, age)
  • satellite.js integration
  • Position calculation with error handling
  • Coordinate conversion (ECI → geodetic)
  • Position validation against thresholds
  • Accuracy validation against API
  • Periodic TLE refresh (every 12 hours)
  • Visibility calculation (daylight/eclipse)
  • Comprehensive test coverage
  • Performance optimization
  • Error logging and monitoring

Common Troubleshooting Issues

Issue 1: Race Condition - TLE Not Initialized

Symptom: Error "TLE not initialized, cannot calculate position" appears when SSE stream starts sending position updates.

Root Cause: SSE connection establishes and starts emitting position events before TLE data has been fetched and initialized in the orbit calculator service.

Impact:

  • Position calculations fail
  • Marker doesn't update
  • Error floods console

Solution: Make ngOnInit async and await TLE initialization before starting SSE connection.

// app.component.ts - INCORRECT (race condition)
ngOnInit(): void {
  // TLE fetch is async, may not complete before SSE starts
  this.orbitCalculator.initializeTLE();

  // SSE starts immediately, calculator not ready!
  this.sseClient.connect();
}

// app.component.ts - CORRECT (awaits initialization)
async ngOnInit(): Promise<void> {
  try {
    // Wait for TLE to be fetched and initialized
    await this.orbitCalculator.initializeTLE();
    console.log('✓ TLE initialized successfully');

    // Only start SSE after calculator is ready
    this.sseClient.connect();
    console.log('✓ SSE connection established');
  } catch (error) {
    console.error('Failed to initialize ISS tracker:', error);
    this.errorMessage = 'Failed to fetch satellite data. Please refresh.';
  }
}

OrbitCalculatorService Pattern:

export class OrbitCalculatorService {
  private satrec: satellite.SatRec | null = null;
  private tleInitialized = false;

  async initializeTLE(): Promise<void> {
    try {
      // Fetch TLE from backend
      const response = await fetch('http://localhost:8000/api/iss/tle');
      if (!response.ok) {
        throw new Error(`TLE fetch failed: ${response.status}`);
      }

      const data = await response.json();

      // Validate and parse
      this.satrec = satellite.twoline2satrec(data.tle.line1, data.tle.line2);
      if (this.satrec.error) {
        throw new Error(`TLE parsing error: ${this.satrec.error}`);
      }

      this.tleInitialized = true;
      console.log('TLE initialized:', data.tle.epoch);

    } catch (error) {
      console.error('TLE initialization failed:', error);
      throw error; // Propagate to caller
    }
  }

  getCurrentPosition(): ISSPosition {
    if (!this.tleInitialized || !this.satrec) {
      throw new Error('TLE not initialized, cannot calculate position');
    }

    // Calculate position...
  }
}

Issue 2: TypeScript Null Check Errors

Symptom: Compilation errors like 'positionAndVelocity' is possibly 'null'

Root Cause: satellite.js propagate() can return null or {position: false} on errors, but TypeScript doesn't know this.

Solution: Add proper null/error checks:

const positionAndVelocity = satellite.propagate(this.satrec, date);

// Check for null return
if (!positionAndVelocity) {
  throw new Error('Propagation returned null');
}

// Check for false position (propagation error)
if ((positionAndVelocity.position as any) === false) {
  throw new Error('Propagation error - position calculation failed');
}

// Now safe to use
const posECI = positionAndVelocity.position as satellite.EciVec3<number>;
const velECI = positionAndVelocity.velocity as satellite.EciVec3<number>;

Issue 3: Incorrect Position Calculation

Symptom: Calculated positions don't match API reference positions (error > 50km)

Common Causes:

  1. Stale TLE data: TLE older than 7 days
  2. Wrong epoch: Using current time instead of TLE epoch for GMST
  3. Coordinate conversion error: Missing or incorrect ECI → geodetic conversion

Debugging Checklist:

// 1. Check TLE age
const tleAge = Date.now() - this.tleEpoch.getTime();
const ageHours = tleAge / (1000 * 60 * 60);
console.log(`TLE age: ${ageHours.toFixed(1)} hours`);
if (ageHours > 168) { // 7 days
  console.warn('TLE data is stale, accuracy degraded');
}

// 2. Verify GMST calculation
const gmst = satellite.gstime(date);
console.log('GMST:', gmst);

// 3. Check ECI coordinates before conversion
console.log('ECI position:', posECI);
console.log('ECI velocity:', velECI);

// 4. Verify geodetic conversion
const gdPos = satellite.eciToGeodetic(posECI, gmst);
console.log('Geodetic:', {
  lat: satellite.degreesLat(gdPos.latitude),
  lon: satellite.degreesLong(gdPos.longitude),
  alt: gdPos.height
});

// 5. Validate against expected ranges
if (gdPos.height < 370 || gdPos.height > 460) {
  console.error('Altitude out of range:', gdPos.height);
}

Issue 4: Performance Degradation

Symptom: Position calculation taking >50ms, causing frame drops

Common Causes:

  1. Re-initializing TLE on every calculation
  2. Creating new Date objects unnecessarily
  3. Not caching GMST calculations

Optimization Pattern:

class OptimizedOrbitCalculator {
  private satrec: satellite.SatRec;
  private lastCalcTime = 0;
  private cachedPosition: ISSPosition | null = null;
  private readonly CACHE_DURATION = 100; // ms

  getCurrentPosition(): ISSPosition {
    const now = Date.now();

    // Return cached result if within 100ms
    if (this.cachedPosition && (now - this.lastCalcTime) < this.CACHE_DURATION) {
      return this.cachedPosition;
    }

    // Calculate new position
    const date = new Date(now);
    const position = this.calculatePosition(date);

    // Cache result
    this.cachedPosition = position;
    this.lastCalcTime = now;

    return position;
  }
}

Issue 5: TLE Checksum Validation Failures

Symptom: Valid TLE data rejected with "Invalid checksum"

Cause: Checksum calculation doesn't account for special characters correctly

Correct Checksum Implementation:

private calculateTLEChecksum(line: string): number {
  // Use only first 68 characters (position 69 is checksum)
  const data = line.substring(0, 68);
  let checksum = 0;

  for (const char of data) {
    if (char >= '0' && char <= '9') {
      checksum += parseInt(char, 10);
    } else if (char === '-') {
      checksum += 1; // Minus sign counts as 1
    }
    // Letters, spaces, periods, + signs don't count
  }

  return checksum % 10;
}

private validateTLEChecksum(line: string): boolean {
  const expectedChecksum = parseInt(line.charAt(68), 10);
  const calculatedChecksum = this.calculateTLEChecksum(line);

  if (calculatedChecksum !== expectedChecksum) {
    console.error('TLE checksum mismatch:', {
      expected: expectedChecksum,
      calculated: calculatedChecksum,
      line: line
    });
    return false;
  }

  return true;
}

Resources