krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Packages/MoonriseKit/Sources/SkyEngine/SkyEngine.swift

59d401dca1a327705632d1e2e20f00f807dee828
moonrise/Packages/MoonriseKit/Sources/SkyEngine/SkyEngine.swift history · blame · raw

218 lines · 9086 bytes

25 symbols in this file
  1import CAstronomy
  2import Foundation
  3
  4/// A place on Earth. Latitude and longitude in degrees (east positive), height in metres.
  5public struct Observer: Sendable, Equatable {
  6    public var latitude: Double
  7    public var longitude: Double
  8    public var height: Double
  9
 10    public init(latitude: Double, longitude: Double, height: Double = 0) {
 11        self.latitude = latitude
 12        self.longitude = longitude
 13        self.height = height
 14    }
 15
 16    var astro: astro_observer_t { Astronomy_MakeObserver(latitude, longitude, height) }
 17}
 18
 19public enum Body: String, CaseIterable, Sendable {
 20    case sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune, pluto
 21
 22    public static let planets: [Body] = [.mercury, .venus, .mars, .jupiter, .saturn, .uranus, .neptune]
 23
 24    var astro: astro_body_t {
 25        switch self {
 26        case .sun: BODY_SUN
 27        case .moon: BODY_MOON
 28        case .mercury: BODY_MERCURY
 29        case .venus: BODY_VENUS
 30        case .mars: BODY_MARS
 31        case .jupiter: BODY_JUPITER
 32        case .saturn: BODY_SATURN
 33        case .uranus: BODY_URANUS
 34        case .neptune: BODY_NEPTUNE
 35        case .pluto: BODY_PLUTO
 36        }
 37    }
 38}
 39
 40/// Apparent right ascension (hours) and declination (degrees) of date, distance in AU.
 41public struct Equatorial: Sendable, Equatable {
 42    public var rightAscension: Double
 43    public var declination: Double
 44    public var distance: Double
 45}
 46
 47/// Altitude above the horizon and azimuth from north through east, both in degrees.
 48public struct Horizontal: Sendable, Equatable {
 49    public var altitude: Double
 50    public var azimuth: Double
 51
 52    public init(altitude: Double, azimuth: Double) {
 53        self.altitude = altitude
 54        self.azimuth = azimuth
 55    }
 56}
 57
 58public struct Illumination: Sendable, Equatable {
 59    public var magnitude: Double
 60    /// Fraction of the disc that is lit, 0...1.
 61    public var phaseFraction: Double
 62}
 63
 64public enum SkyEngineError: Error, Equatable {
 65    case astronomy(status: Int)
 66}
 67
 68/// Positions and phases from Astronomy Engine. Its C code keeps internal caches,
 69/// so every call into it holds one process-wide lock.
 70public enum SkyEngine {
 71    static let lock = NSRecursiveLock()
 72
 73    static func time(_ date: Date) -> astro_time_t {
 74        DeltaT.installed
 75        // Days since J2000 (2000-01-01 12:00 UTC).
 76        return Astronomy_TimeFromDays((date.timeIntervalSince1970 - 946_728_000) / 86_400)
 77    }
 78
 79    static func check(_ status: astro_status_t) throws {
 80        guard status == ASTRO_SUCCESS else { throw SkyEngineError.astronomy(status: Int(status.rawValue)) }
 81    }
 82
 83    public static func equatorial(_ body: Body, at date: Date, from observer: Observer) throws -> Equatorial {
 84        lock.lock()
 85        defer { lock.unlock() }
 86        var t = time(date)
 87        let eq = Astronomy_Equator(body.astro, &t, observer.astro, EQUATOR_OF_DATE, ABERRATION)
 88        try check(eq.status)
 89        return Equatorial(rightAscension: eq.ra, declination: eq.dec, distance: eq.dist)
 90    }
 91
 92    public static func horizontal(_ body: Body, at date: Date, from observer: Observer, refraction: Bool = false) throws -> Horizontal {
 93        let eq = try equatorial(body, at: date, from: observer)
 94        return horizontal(rightAscension: eq.rightAscension, declination: eq.declination, at: date, from: observer, refraction: refraction)
 95    }
 96
 97    /// For fixed objects such as stars: RA in hours, declination in degrees, both of date.
 98    public static func horizontal(rightAscension: Double, declination: Double, at date: Date, from observer: Observer, refraction: Bool = false) -> Horizontal {
 99        lock.lock()
100        defer { lock.unlock() }
101        var t = time(date)
102        let hor = Astronomy_Horizon(&t, observer.astro, rightAscension, declination, refraction ? REFRACTION_NORMAL : REFRACTION_NONE)
103        return Horizontal(altitude: hor.altitude, azimuth: hor.azimuth)
104    }
105
106    /// Horizontal positions for many fixed objects at once, from J2000 RA and declination in degrees.
107    /// Applies precession and nutation; no aberration or refraction.
108    /// 2000-01-01 12:00 TT.
109    public static let j2000 = Date(timeIntervalSince1970: 946_727_935.816)
110
111    /// Right ascension and declination on the equator and equinox of `date` (precession and nutation applied), degrees.
112    public static func equatorOfDate(j2000 c: (ra: Double, dec: Double), at date: Date) -> (ra: Double, dec: Double) {
113        lock.lock()
114        defer { lock.unlock() }
115        var t = time(date)
116        let rotation = Astronomy_Rotation_EQJ_EQD(&t)
117        let ra = c.ra * .pi / 180, dec = c.dec * .pi / 180
118        let v = Astronomy_RotateVector(rotation, astro_vector_t(status: ASTRO_SUCCESS, x: cos(dec) * cos(ra), y: cos(dec) * sin(ra), z: sin(dec), t: t))
119        var r = atan2(v.y, v.x) * 180 / .pi
120        if r < 0 { r += 360 }
121        return (r, asin(v.z) * 180 / .pi)
122    }
123
124    public static func horizontal(j2000 coordinates: [(ra: Double, dec: Double)], at date: Date, from observer: Observer) -> [Horizontal] {
125        lock.lock()
126        defer { lock.unlock() }
127        var t = time(date)
128        let rotation = Astronomy_Rotation_EQJ_HOR(&t, observer.astro)
129        return coordinates.map { c in
130            let ra = c.ra * .pi / 180, dec = c.dec * .pi / 180
131            let v = astro_vector_t(status: ASTRO_SUCCESS, x: cos(dec) * cos(ra), y: cos(dec) * sin(ra), z: sin(dec), t: t)
132            let s = Astronomy_HorizonFromVector(Astronomy_RotateVector(rotation, v), REFRACTION_NONE)
133            return Horizontal(altitude: s.lat, azimuth: s.lon)
134        }
135    }
136
137    /// Heliocentric position in AU, J2000 equatorial axes.
138    public static func heliocentric(_ body: Body, at date: Date) throws -> SIMD3<Double> {
139        lock.lock()
140        defer { lock.unlock() }
141        let v = Astronomy_HelioVector(body.astro, time(date))
142        try check(v.status)
143        return SIMD3(v.x, v.y, v.z)
144    }
145
146    /// Heliocentric position in AU on J2000 ecliptic axes (x toward the equinox, z toward the ecliptic north pole).
147    public static func heliocentricEcliptic(_ body: Body, at date: Date) throws -> SIMD3<Double> {
148        ecliptic(try heliocentric(body, at: date))
149    }
150
151    /// Earth's heliocentric position in AU on J2000 ecliptic axes.
152    public static func heliocentricEarth(at date: Date) throws -> SIMD3<Double> {
153        lock.lock()
154        defer { lock.unlock() }
155        let v = Astronomy_HelioVector(BODY_EARTH, time(date))
156        try check(v.status)
157        return ecliptic(SIMD3(v.x, v.y, v.z))
158    }
159
160    /// The Moon's geocentric position in km on J2000 ecliptic axes (as JPL Horizons gives with REF_PLANE ECLIPTIC).
161    public static func geocentricMoonEcliptic(at date: Date) -> SIMD3<Double> {
162        lock.lock()
163        defer { lock.unlock() }
164        let v = Astronomy_GeoMoon(time(date))
165        return ecliptic(SIMD3(v.x, v.y, v.z)) * 1.495978707e8
166    }
167
168    /// J2000 equatorial to J2000 ecliptic axes.
169    static func ecliptic(_ v: SIMD3<Double>) -> SIMD3<Double> {
170        let e = 23.4392911 * Double.pi / 180
171        return SIMD3(v.x, v.y * cos(e) + v.z * sin(e), -v.y * sin(e) + v.z * cos(e))
172    }
173
174    public static func illumination(_ body: Body, at date: Date) throws -> Illumination {
175        lock.lock()
176        defer { lock.unlock() }
177        let i = Astronomy_Illumination(body.astro, time(date))
178        try check(i.status)
179        return Illumination(magnitude: i.mag, phaseFraction: i.phase_fraction)
180    }
181
182    static func date(_ t: astro_time_t) -> Date {
183        Date(timeIntervalSince1970: 946_728_000 + t.ut * 86_400)
184    }
185
186    /// Next rise and next set within `limitDays` after `date`; nil if none in that window.
187    public static func riseSet(_ body: Body, after date: Date, from observer: Observer, limitDays: Double = 1) -> (rise: Date?, set: Date?) {
188        lock.lock()
189        defer { lock.unlock() }
190        let t = time(date)
191        let rise = Astronomy_SearchRiseSetEx(body.astro, observer.astro, DIRECTION_RISE, t, limitDays, 0)
192        let set = Astronomy_SearchRiseSetEx(body.astro, observer.astro, DIRECTION_SET, t, limitDays, 0)
193        return (rise.status == ASTRO_SUCCESS ? Self.date(rise.time) : nil,
194                set.status == ASTRO_SUCCESS ? Self.date(set.time) : nil)
195    }
196
197    public enum MoonQuarter: Int, Sendable {
198        case new, firstQuarter, full, lastQuarter
199    }
200
201    /// The first lunar quarter after `date`.
202    public static func nextMoonQuarter(after date: Date) throws -> (quarter: MoonQuarter, date: Date) {
203        lock.lock()
204        defer { lock.unlock() }
205        let q = Astronomy_SearchMoonQuarter(time(date))
206        try check(q.status)
207        return (MoonQuarter(rawValue: Int(q.quarter))!, Self.date(q.time))
208    }
209
210    /// Moon's ecliptic longitude east of the sun in degrees: 0 new, 90 first quarter, 180 full, 270 last quarter.
211    public static func moonPhase(at date: Date) throws -> Double {
212        lock.lock()
213        defer { lock.unlock() }
214        let a = Astronomy_MoonPhase(time(date))
215        try check(a.status)
216        return a.angle
217    }
218}