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/Tonight.swift

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

82 lines · 3964 bytes

3 symbols in this file
 1import Foundation
 2
 3public struct PlanetTonight: Sendable {
 4    public var body: Body
 5    public var magnitude: Double
 6    public var now: Horizontal
 7    /// Highest point during the night, if it gets above 10°.
 8    public var best: (date: Date, position: Horizontal)?
 9    public var rise: Date?
10    public var set: Date?
11}
12
13public struct Tonight: Sendable {
14    /// Darkness window: from now if it is already dark, else from the next sunset.
15    public var start: Date
16    public var end: Date
17    public var sunset: Date?
18    public var sunrise: Date?
19    /// Astronomical dusk to dawn (Sun below −18°) within start...end; nil where it never gets that dark.
20    public var dark: DateInterval?
21    /// When the Moon is above the horizon within start...end.
22    public var moonUp: [DateInterval]
23    public var moonIllumination: Double
24    public var moonPhase: Double
25    public var nextQuarter: (quarter: SkyEngine.MoonQuarter, date: Date)
26    /// Naked-eye planets, those visible tonight first, brightest first.
27    public var planets: [PlanetTonight]
28
29    public init(at date: Date, from observer: Observer) throws {
30        let sunNow = try SkyEngine.horizontal(.sun, at: date, from: observer, refraction: true)
31        let sunToday = SkyEngine.riseSet(.sun, after: date, from: observer)
32        if sunNow.altitude < -0.8 {
33            start = date
34            sunset = nil
35        } else {
36            sunset = sunToday.set
37            start = sunToday.set ?? date
38        }
39        sunrise = SkyEngine.riseSet(.sun, after: start, from: observer).rise
40        end = sunrise ?? start.addingTimeInterval(12 * 3600)
41        dark = try SkyEngine.darkness(from: start, to: end, observer: observer)
42        moonUp = try SkyEngine.moonUp(from: start, to: end, observer: observer)
43
44        moonIllumination = try SkyEngine.illumination(.moon, at: date).phaseFraction
45        moonPhase = try SkyEngine.moonPhase(at: date)
46        nextQuarter = try SkyEngine.nextMoonQuarter(after: date)
47
48        let samples = stride(from: start.timeIntervalSince1970, through: end.timeIntervalSince1970, by: 900).map(Date.init(timeIntervalSince1970:))
49        planets = try [Body.mercury, .venus, .mars, .jupiter, .saturn].map { body in
50            var best: (Date, Horizontal)?
51            for s in samples {
52                let h = try SkyEngine.horizontal(body, at: s, from: observer, refraction: true)
53                if h.altitude > 10, h.altitude > best?.1.altitude ?? -90 { best = (s, h) }
54            }
55            let rs = SkyEngine.riseSet(body, after: date, from: observer)
56            return PlanetTonight(
57                body: body,
58                magnitude: try SkyEngine.illumination(body, at: date).magnitude,
59                now: try SkyEngine.horizontal(body, at: date, from: observer, refraction: true),
60                best: best.map { (date: $0.0, position: $0.1) },
61                rise: rs.rise,
62                set: rs.set
63            )
64        }
65        .sorted { ($0.best != nil ? 0 : 1, $0.magnitude) < ($1.best != nil ? 0 : 1, $1.magnitude) }
66    }
67
68    /// Moonrise and moonset during the night, if any.
69    public var moonrise: Date? { moonUp.first { $0.start > start }?.start }
70    public var moonset: Date? { moonUp.first { $0.end < end }?.end }
71
72    /// Whether the Moon is up during darkness (or the night, where it never gets fully dark) and lit enough to hide faint objects.
73    public var moonlit: Bool {
74        let window = dark ?? DateInterval(start: start, end: end)
75        return moonIllumination >= SkyEngine.moonlightLimit && moonUp.contains { ($0.intersection(with: window)?.duration ?? 0) > 0 }
76    }
77
78    /// "Dark 20:05–05:42. Moon 62% lit, sets 23:10."
79    public func moonAndDarkness(time: Date.FormatStyle = .init(date: .omitted, time: .shortened)) -> String {
80        SkyEngine.moonAndDarkness(night: DateInterval(start: start, end: end), dark: dark, moonUp: moonUp, moonIllumination: moonIllumination, time: time)
81    }
82}