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

73 lines · 4049 bytes

4 symbols in this file
 1import CAstronomy
 2import Foundation
 3
 4extension SkyEngine {
 5    /// Moon illumination from which moonlight is taken to hide faint satellites and stars.
 6    public static let moonlightLimit = 0.25
 7
 8    /// Next time within `limitDays` after `date` that the body's centre crosses `altitude` degrees going up
 9    /// (`rising`) or down, without refraction (the convention for twilight); nil if it does not.
10    public static func searchAltitude(_ body: Body, altitude: Double, rising: Bool, after date: Date, from observer: Observer, limitDays: Double = 1) -> Date? {
11        lock.lock()
12        defer { lock.unlock() }
13        let r = Astronomy_SearchAltitude(body.astro, observer.astro, rising ? DIRECTION_RISE : DIRECTION_SET, time(date), limitDays, altitude)
14        return r.status == ASTRO_SUCCESS ? Self.date(r.time) : nil
15    }
16
17    /// Astronomical darkness (Sun below −18°) between `start` and `end`, clipped to them; nil if it never gets that dark.
18    public static func darkness(from start: Date, to end: Date, observer: Observer) throws -> DateInterval? {
19        let days = end.timeIntervalSince(start) / 86_400
20        let dusk = try horizontal(.sun, at: start, from: observer).altitude < -18
21            ? start
22            : searchAltitude(.sun, altitude: -18, rising: false, after: start, from: observer, limitDays: days)
23        guard let dusk, dusk < end else { return nil }
24        let dawn = searchAltitude(.sun, altitude: -18, rising: true, after: dusk, from: observer, limitDays: days).map { min($0, end) } ?? end
25        return DateInterval(start: dusk, end: dawn)
26    }
27
28    /// When the Moon is above the horizon between `start` and `end`, clipped to them.
29    public static func moonUp(from start: Date, to end: Date, observer: Observer) throws -> [DateInterval] {
30        var out: [DateInterval] = []
31        var t = start
32        while t < end {
33            let days = end.timeIntervalSince(t) / 86_400
34            let next = riseSet(.moon, after: t, from: observer, limitDays: days)
35            if let set = next.set, next.rise.map({ set < $0 }) ?? true {
36                out.append(DateInterval(start: t, end: set))
37                t = set.addingTimeInterval(60)
38            } else if let rise = next.rise {
39                let set = riseSet(.moon, after: rise, from: observer, limitDays: end.timeIntervalSince(rise) / 86_400).set ?? end
40                out.append(DateInterval(start: rise, end: set))
41                t = set.addingTimeInterval(60)
42            } else {
43                if try horizontal(.moon, at: t, from: observer, refraction: true).altitude > 0 { out.append(DateInterval(start: t, end: end)) }
44                break
45            }
46        }
47        return out
48    }
49
50    /// "Dark 20:05–05:42. Moon 62% lit, sets 23:10." The Moon's rise and set are those during darkness,
51    /// or during `night` where it never gets fully dark.
52    public static func moonAndDarkness(night: DateInterval, dark: DateInterval?, moonUp: [DateInterval], moonIllumination: Double,
53                                       time: Date.FormatStyle = .init(date: .omitted, time: .shortened)) -> String {
54        let window = dark ?? night
55        let up = moonUp.compactMap { $0.intersection(with: window) }.filter { $0.duration > 0 }
56        let first = dark.map { "Dark \($0.start.formatted(time))–\($0.end.formatted(time))." } ?? "The sky does not get fully dark."
57        let lit = "Moon \(Int((moonIllumination * 100).rounded()))% lit"
58        let moon: String
59        if up.isEmpty {
60            moon = "Moon down throughout."
61        } else if up.count == 1, up[0] == window {
62            moon = "\(lit), up throughout."
63        } else {
64            var times: [String] = []
65            for i in up {
66                if i.start > window.start { times.append("rises \(i.start.formatted(time))") }
67                if i.end < window.end { times.append("sets \(i.end.formatted(time))") }
68            }
69            moon = "\(lit), \(times.joined(separator: ", "))."
70        }
71        return "\(first) \(moon)"
72    }
73}