Skip to content

mvdmakesthings/Ephemeris

Repository files navigation

Ephemeris

CI License Platform Swift

A Swift framework for satellite tracking and orbital mechanics calculations. Ephemeris provides tools to parse Two-Line Element (TLE) data and calculate orbital positions for Earth-orbiting satellites.

Dual-Purpose Design: Ephemeris serves both as a practical Swift framework for iOS developers building satellite tracking apps and as an educational tool for learning orbital mechanics through hands-on implementation. Each feature is documented with both mathematical foundations and Swift code examples.

Table of Contents

Features

  • 📡 TLE Parsing: Parse NORAD Two-Line Element (TLE) format satellite data
  • 🛰️ Orbital Calculations: Calculate satellite positions using orbital mechanics
  • 🌍 Position Tracking: Compute latitude, longitude, and altitude for satellites at any given time
  • 👁️ Observer-Based Tracking: Calculate azimuth, elevation, range, and range rate from any location on Earth
  • 🔭 Pass Prediction: Predict satellite passes with AOS, maximum elevation, and LOS times
  • 📐 Orbital Elements: Support for all standard Keplerian orbital elements:
    • Semi-major axis
    • Eccentricity
    • Inclination
    • Right Ascension of Ascending Node (RAAN)
    • Argument of Perigee
    • Mean Anomaly and True Anomaly
  • 🌐 Coordinate Transformations: ECI ↔ ECEF ↔ ENU coordinate system conversions
  • 🌫️ Atmospheric Refraction: Optional correction for low-elevation observations
  • Time Conversions: Julian date and Greenwich Sidereal Time calculations
  • 🔢 High Precision: Iterative algorithms for eccentric anomaly calculations

Requirements

  • iOS 16.0+ / macOS 13.0+ / watchOS 9.0+ / tvOS 16.0+ / visionOS 1.0+
  • Swift 6.0+

Platform Support Policy: Ephemeris follows a "current minus two" support policy, supporting the current OS version minus two releases for broad compatibility while maintaining access to modern APIs.

Installation

Swift Package Manager

Add Ephemeris to your Package.swift dependencies:

dependencies: [
    .package(url: "https://github.com/mvdmakesthings/Ephemeris.git", from: "1.0.0")
]

Then add it to your target dependencies:

targets: [
    .target(
        name: "YourTarget",
        dependencies: ["Ephemeris"]
    )
]

Or in Xcode:

  1. File → Add Packages...
  2. Enter: https://github.com/mvdmakesthings/Ephemeris.git
  3. Select version and click "Add Package"

Manual Integration

  1. Download the source code
  2. Drag the Ephemeris folder into your Xcode project
  3. Ensure the files are added to your target

Usage

Quick Start

import Ephemeris

// TLE data for the International Space Station (ISS)
let tleString = """
ISS (ZARYA)
1 25544U 98067A   20097.82871450  .00000874  00000-0  24271-4 0  9992
2 25544  51.6465 341.5807 0003880  94.4223  26.1197 15.48685836220958
"""

// Parse the TLE data
do {
    let tle = try TwoLineElement(from: tleString)
    print("Satellite: \(tle.name)")
    
    // Create an orbit from the TLE
    let orbit = Orbit(from: tle)
    
    // Calculate current position
    let position = try orbit.calculatePosition(at: Date())
    print("Latitude: \(position.latitude)°")
    print("Longitude: \(position.longitude)°")
    print("Altitude: \(position.altitude) km")
} catch {
    print("Error: \(error)")
}

Accessing Orbital Elements

let orbit = Orbit(from: tle)

// Access orbital parameters
print("Semi-major axis: \(orbit.semimajorAxis) km")
print("Eccentricity: \(orbit.eccentricity)")
print("Inclination: \(orbit.inclination)°")
print("RAAN: \(orbit.rightAscensionOfAscendingNode)°")
print("Argument of Perigee: \(orbit.argumentOfPerigee)°")
print("Mean Anomaly: \(orbit.meanAnomaly)°")
print("Mean Motion: \(orbit.meanMotion) revolutions/day")

Calculate Position at Specific Time

// Create a specific date
let calendar = Calendar.current
let components = DateComponents(year: 2020, month: 4, day: 15, hour: 12, minute: 0)
let specificDate = calendar.date(from: components)

// Calculate position at that time
if let date = specificDate {
    let position = try? orbit.calculatePosition(at: date)
    if let pos = position {
        print("At \(date):")
        print("  Latitude: \(pos.latitude)°")
        print("  Longitude: \(pos.longitude)°")
        print("  Altitude: \(pos.altitude) km")
    }
}

Track Satellite Over Time

import Foundation

// Track satellite position every minute for an hour
let startTime = Date()
let timeInterval: TimeInterval = 60 // seconds

for i in 0..<60 {
    let time = startTime.addingTimeInterval(Double(i) * timeInterval)
    
    do {
        let position = try orbit.calculatePosition(at: time)
        print("T+\(i) min: \(position.latitude)°, \(position.longitude)°, \(position.altitude) km")
    } catch {
        print("Error calculating position: \(error)")
    }
}

Multiple Satellites

// Track multiple satellites
let satellites = [
    ("ISS", issТleString),
    ("GOES-16", goes16TleString),
    ("GPS BIIF-1", gpsTleString)
]

for (name, tleString) in satellites {
    do {
        let tle = try TwoLineElement(from: tleString)
        let orbit = Orbit(from: tle)
        let position = try orbit.calculatePosition(at: Date())
        
        print("\(name):")
        print("  Position: \(position.latitude)°, \(position.longitude)°")
        print("  Altitude: \(position.altitude) km")
        print()
    } catch {
        print("Error processing \(name): \(error)")
    }
}

Error Handling

// Comprehensive error handling
let tleString = """
SATELLITE NAME
1 12345U 20001A   20100.50000000  .00001234  00000-0  12345-4 0  9999
2 12345  51.6400  90.0000 0001000  45.0000  90.0000 15.50000000123456
"""

do {
    let tle = try TwoLineElement(from: tleString)
    let orbit = Orbit(from: tle)
    let position = try orbit.calculatePosition(at: Date())
    
    print("Successfully calculated position: \(position.latitude)°, \(position.longitude)°")
    
} catch TLEParsingError.invalidFormat(let message) {
    print("Invalid TLE format: \(message)")
} catch TLEParsingError.invalidChecksum(let line, let expected, let actual) {
    print("Checksum error on line \(line): expected \(expected), got \(actual)")
} catch TLEParsingError.invalidNumber(let field, let value) {
    print("Invalid number in field '\(field)': \(value)")
} catch CalculationError.reachedSingularity {
    print("Cannot calculate orbit: eccentricity >= 1.0 (not an elliptical orbit)")
} catch {
    print("Unexpected error: \(error)")
}

Working with Julian Dates and Sidereal Time

import Foundation

// Convert current date to Julian Day
if let julianDay = Date.julianDay(from: Date()) {
    print("Current Julian Day: \(julianDay)")
    
    // Calculate Greenwich Sidereal Time
    let gst = Date.greenwichSideRealTime(from: julianDay)
    print("Greenwich Sidereal Time: \(gst) radians")
    
    // Convert to J2000 epoch
    let j2000 = Date.toJ2000(from: julianDay)
    print("Julian centuries since J2000: \(j2000)")
}

// Convert TLE epoch to Julian Day
let epochJD = Date.julianDayFromEpoch(epochYear: 2020, epochDayFraction: 97.82871450)
print("TLE Epoch as Julian Day: \(epochJD)")

Predict Satellite Passes

Calculate when and where a satellite will be visible from your location:

import Ephemeris

// Parse ISS TLE
let tleString = """
ISS (ZARYA)
1 25544U 98067A   20097.82871450  .00000874  00000-0  24271-4 0  9992
2 25544  51.6465 341.5807 0003880  94.4223  26.1197 15.48685836220958
"""
let tle = try TwoLineElement(from: tleString)
let orbit = Orbit(from: tle)

// Define your observer location (Louisville, Kentucky)
let observer = Observer(
    latitudeDeg: 38.2542,    // Latitude (positive = north)
    longitudeDeg: -85.7594,  // Longitude (positive = east)
    altitudeMeters: 140      // Altitude above sea level
)

// Predict passes over the next 24 hours
let now = Date()
let tomorrow = now.addingTimeInterval(24 * 3600)

let passes = try orbit.predictPasses(
    for: observer,
    from: now,
    to: tomorrow,
    minElevationDeg: 10.0,  // Only passes above 10° elevation
    stepSeconds: 30          // Search granularity
)

// Display results
for (i, pass) in passes.enumerated() {
    print("\nPass #\(i + 1)")
    print("AOS: \(pass.aos.time)")
    print("  Azimuth: \(pass.aos.azimuthDeg)°")
    print("MAX: \(pass.max.time)")
    print("  Elevation: \(pass.max.elevationDeg)°")
    print("  Azimuth: \(pass.max.azimuthDeg)°")
    print("LOS: \(pass.los.time)")
    print("  Azimuth: \(pass.los.azimuthDeg)°")
    print("Duration: \(Int(pass.duration)) seconds")
}

Calculate Topocentric Coordinates

Get azimuth, elevation, and range for a satellite at any time:

// Calculate current look angles
let topo = try orbit.topocentric(at: Date(), for: observer)

print("Satellite Position:")
print("  Azimuth: \(topo.azimuthDeg)°")        // Direction (0° = North, 90° = East)
print("  Elevation: \(topo.elevationDeg)°")    // Angle above horizon
print("  Range: \(topo.rangeKm) km")           // Distance to satellite
print("  Range Rate: \(topo.rangeRateKmPerSec) km/s")  // Approaching/receding

// Apply atmospheric refraction correction for low elevations
let topoRefracted = try orbit.topocentric(
    at: Date(),
    for: observer,
    applyRefraction: true
)
print("Apparent Elevation (with refraction): \(topoRefracted.elevationDeg)°")

Custom Satellite Analysis

// Analyze orbital characteristics
func analyzeOrbit(_ orbit: Orbit) {
    let earthRadius = PhysicalConstants.Earth.radius
    
    // Calculate apogee and perigee
    let apogee = orbit.semimajorAxis * (1 + orbit.eccentricity) - earthRadius
    let perigee = orbit.semimajorAxis * (1 - orbit.eccentricity) - earthRadius
    
    print("Orbital Analysis:")
    print("  Semi-major axis: \(orbit.semimajorAxis) km")
    print("  Eccentricity: \(orbit.eccentricity)")
    print("  Apogee altitude: \(apogee) km")
    print("  Perigee altitude: \(perigee) km")
    print("  Inclination: \(orbit.inclination)°")
    
    // Determine orbit type
    if orbit.inclination < 10 {
        print("  Type: Equatorial orbit")
    } else if orbit.inclination > 80 && orbit.inclination < 100 {
        print("  Type: Polar orbit")
    } else {
        print("  Type: Inclined orbit")
    }
    
    // Calculate orbital period
    let mu = PhysicalConstants.Earth.µ
    let period = 2 * .pi * sqrt(pow(orbit.semimajorAxis, 3) / mu)
    print("  Orbital period: \(period / 60) minutes")
}

// Use the analyzer
let tle = try TwoLineElement(from: tleString)
let orbit = Orbit(from: tle)
analyzeOrbit(orbit)

Documentation

Ephemeris documentation is designed to teach orbital mechanics through practical Swift implementation. Choose your learning path:

📚 Choose Your Path

🚀 Quick Start: "I want to build an app NOW"

  1. Getting Started Guide - Build your first satellite tracker in 30 minutes
  2. API Reference - Complete API documentation
  3. Jump to specific guides as needed

🎓 Deep Dive: "I want to understand orbital mechanics"

  1. Orbital Elements - The six Keplerian elements with math and Swift
  2. Observer Geometry - Coordinate transformations and pass prediction
  3. Visualization - Ground tracks, sky tracks, and iOS integration
  4. Coordinate Systems - Deep dive into ECI, ECEF, and transformations

🔍 Reference: "I need specific information"

📖 Documentation Overview

Theory + Practice Documents (Math → Swift implementation):

  • Orbital Elements - Keplerian elements, TLE format, Kepler's equation, accuracy considerations
  • Observer Geometry - Coordinate transformations, topocentric calculations, pass prediction algorithms
  • Visualization - Ground tracks, sky tracks, SwiftUI Charts, and MapKit integration

Practical Guides (Code-focused):

Reference:

Core Types

  • TwoLineElement: Parses and represents NORAD TLE format satellite data
  • Orbit: Represents orbital parameters and provides position calculation methods
  • Observer: Represents an Earth-based observer location (latitude, longitude, altitude)
  • Topocentric: Contains azimuth, elevation, range, and range rate for observer-relative coordinates
  • PassWindow: Describes a satellite pass with AOS, maximum elevation, and LOS details
  • CoordinateTransforms: Utility functions for converting between coordinate systems (ECI, ECEF, ENU)

Where to Get TLE Data

TLE data for satellites can be obtained from:

Key Concepts

TLE Format: Uses 2-digit years with ±50 year window. Ephemeris automatically handles date interpretation for current satellite tracking (designed for recent TLE data).

Accuracy: Best within 1-3 days of TLE epoch. Update TLEs regularly for mission-critical applications (every 1-3 days for LEO satellites).

Propagation: Uses Keplerian orbital mechanics (two-body problem). Does not include atmospheric drag, solar radiation pressure, or perturbations. See Orbital Elements for detailed accuracy discussion.

For AI Tools and Developers

For developers using AI-assisted coding tools (ChatGPT, Claude, GitHub Copilot), Ephemeris includes an LLM.txt file that provides natural-language context about the project's purpose, architecture, and design goals. This helps large language models better understand the framework when providing code suggestions, generating documentation, or answering questions about the codebase.

The LLM.txt file includes:

  • High-level overview of what Ephemeris is and what it isn't
  • Descriptions of core components and their relationships
  • Design philosophy and implementation approach
  • Intended use cases and limitations
  • Future roadmap features

This context-aware documentation improves the accuracy of AI-generated code and explanations when working with Ephemeris.

CI/CD

This project uses GitHub Actions for continuous integration:

  • Build and Test: Automatically builds the framework and runs all tests on every push and pull request using Swift Package Manager
  • SwiftLint: Enforces Swift style and conventions in strict mode

Running Tests Locally

Ephemeris uses XCTest (Apple's standard testing framework) for all tests.

# Build the package
swift build

# Run tests
swift test

# Run tests with verbose output
swift test --verbose

The test suite includes 122 tests covering:

  • TLE parsing and validation
  • Orbital calculations and Kepler's equation
  • Coordinate transformations (ECI, ECEF, Geodetic, ENU)
  • Observer-relative calculations (topocentric coordinates)
  • Pass prediction algorithms
  • Ground track and sky track generation

All tests must pass before submitting a pull request.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines on:

  • Development setup
  • Code style and conventions
  • Testing requirements
  • Documentation standards
  • Pull request process

Quick Start for Contributors

# Fork and clone
git clone https://github.com/YOUR_USERNAME/Ephemeris.git
cd Ephemeris

# Create a feature branch
git checkout -b feature/amazing-feature

# Build and test
swift build
swift test

# Run SwiftLint
swiftlint lint --strict

# Make changes, commit, and push
git commit -m 'Add amazing feature'
git push origin feature/amazing-feature

For major changes, please open an issue first to discuss what you would like to change.

References

Satellite Tracking and Orbital Mechanics

Sidereal Time and Julian Date Calculations

License

This project is licensed under the Apache License 2.0 - see the LICENSE.md file for details.

Copyright © 2020 Michael VanDyke

Acknowledgements

Special thanks to all the researchers, institutions, and open source projects that made this work possible.

Open Source Projects

  • ZeiSatTrack [Apache 2.0] - Reference for rotation math and Julian date conversion calculations

About

Ephemeris is a Swift framework for satellite tracking and orbital mechanics calculations. It provides tools to parse Two-Line Element (TLE) data and calculate orbital positions for Earth-orbiting satellites. The framework is dual-purpose: both a practical Swift library for iOS/macOS developers and an educational tool for learning orbital mechanics.

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors