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.
- Features
- Requirements
- Installation
- Usage
- Documentation
- For AI Tools and Developers
- CI/CD
- Contributing
- References
- License
- Acknowledgements
- 📡 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
- 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.
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:
- File → Add Packages...
- Enter:
https://github.com/mvdmakesthings/Ephemeris.git - Select version and click "Add Package"
- Download the source code
- Drag the
Ephemerisfolder into your Xcode project - Ensure the files are added to your target
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)")
}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")// 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")
}
}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)")
}
}// 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)")
}
}// 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)")
}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)")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")
}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)°")// 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)Ephemeris documentation is designed to teach orbital mechanics through practical Swift implementation. Choose your learning path:
- Getting Started Guide - Build your first satellite tracker in 30 minutes
- API Reference - Complete API documentation
- Jump to specific guides as needed
- Orbital Elements - The six Keplerian elements with math and Swift
- Observer Geometry - Coordinate transformations and pass prediction
- Visualization - Ground tracks, sky tracks, and iOS integration
- Coordinate Systems - Deep dive into ECI, ECEF, and transformations
- API Reference - All types, methods, and properties
- Testing Guide - Testing patterns with Spectre
- LLM.txt - Project context for AI tools
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):
- Getting Started - Quick-start tutorial with complete examples
- Testing Guide - Unit testing patterns for orbital mechanics
Reference:
- API Reference - Complete API documentation
- Coordinate Systems - Mathematical foundations of coordinate transformations
TwoLineElement: Parses and represents NORAD TLE format satellite dataOrbit: Represents orbital parameters and provides position calculation methodsObserver: Represents an Earth-based observer location (latitude, longitude, altitude)Topocentric: Contains azimuth, elevation, range, and range rate for observer-relative coordinatesPassWindow: Describes a satellite pass with AOS, maximum elevation, and LOS detailsCoordinateTransforms: Utility functions for converting between coordinate systems (ECI, ECEF, ENU)
TLE data for satellites can be obtained from:
- CelesTrak - Free, updated frequently
- Space-Track.org - Official source (free registration required)
- N2YO.com - Real-time tracking and TLE data
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 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.
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
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 --verboseThe 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.
Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines on:
- Development setup
- Code style and conventions
- Testing requirements
- Documentation standards
- Pull request process
# 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-featureFor major changes, please open an issue first to discuss what you would like to change.
- Satellite Tracking Using NORAD Two-Line Element Set Format - Transilvania University of Braşov, by Emilian-Ionuţ CROITORU and Gheorghe OANCEA
- Calculation of Satellite Position from Ephemeris Data - Applied GPS for Engineers and Project Managers, ascelibrary.org
- Describing Orbits - FAA US Government
- Transformation of Orbit Elements - Space Electronic Reconnaissance: Localization Theories and Methods
- Introduction to Orbital Mechanics - W. Horn, B. Shapiro, C. Shubin, F. Varedi, California State University Northridge
- Computation of Sub-Satellite Points from Orbital Elements - Richard H. Christ, NASA
- Satellite Orbits - Calculus Castle
- Methods of Astrodynamics: A Computer Approach
- Sidereal Time
- Revisiting Spacetrack Report #3 - Celestrak
- Computing Julian Date - AAVSO
This project is licensed under the Apache License 2.0 - see the LICENSE.md file for details.
Copyright © 2020 Michael VanDyke
Special thanks to all the researchers, institutions, and open source projects that made this work possible.
- ZeiSatTrack [Apache 2.0] - Reference for rotation math and Julian date conversion calculations