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/Answers/FeedAnswers.swift

b327e0d6e7ab67511c5bade3f3c3a93834f6e9ca
moonrise/Packages/MoonriseKit/Sources/Answers/FeedAnswers.swift history · blame · raw

79 lines · 3623 bytes

 1import Feeds
 2import Foundation
 3import SkyEngine
 4
 5/// Kp now and over the next day, and whether aurora may be seen from here.
 6public struct AuroraAnswer: Sendable {
 7    public var kpNow: Double
 8    public var kpNext24h: Double?
 9    public var observedAt: Date
10    /// Kp at which aurora may be seen on the horizon from this place.
11    public var kpNeeded: Double
12    public var possible: Bool
13    public var rows: [AnswerRow]
14    public var footnote: String
15    public var spoken: String
16
17    static func level(_ kp: Double) -> String {
18        if let g = SpaceWeather.gScale(kp) { return "G\(g) storm" }
19        return kp < 4 ? "quiet" : "active"
20    }
21
22    public init(weather: SpaceWeather, place: Place, locale: Locale = .current, timeZone: TimeZone = .current) {
23        let say = Phrasing(locale: locale, timeZone: timeZone)
24        kpNow = weather.kpNow
25        kpNext24h = weather.kpNext24h
26        observedAt = weather.observedAt
27        kpNeeded = Geomagnetic.kpNeeded(place.observer)
28        possible = max(kpNow, kpNext24h ?? 0) >= kpNeeded
29        let horizon = Geomagnetic.latitude(place.observer) < 0 ? "southern" : "northern"
30
31        let gNow = SpaceWeather.gScale(kpNow)
32        rows = [
33            AnswerRow("Kp now", say.number(kpNow, digits: 1) + (gNow.map { " · G\($0)" } ?? ""), alert: gNow != nil),
34            AnswerRow("Next 24 h", kpNext24h.map { "up to \(say.number($0, digits: 1))" } ?? "–", alert: kpNext24h.flatMap(SpaceWeather.gScale) != nil),
35            AnswerRow("Aurora", possible ? "possible, \(horizon) horizon" : "unlikely here"),
36        ]
37        footnote = "NOAA SWPC · \(say.time(observedAt))"
38
39        var lines = ["Kp \(say.number(kpNow, digits: 1)), \(Self.level(kpNow))."]
40        if let next = kpNext24h, next > kpNow { lines.append("Up to Kp \(say.number(next, digits: 1)) in the next 24 hours.") }
41        if possible {
42            lines.append("Aurora possible low on the \(horizon) horizon.")
43        } else if kpNeeded > 9 {
44            lines.append("Aurora is out of reach this far from the poles.")
45        } else {
46            lines.append("Aurora unlikely here. You'd need about Kp \(Int(kpNeeded.rounded(.up))).")
47        }
48        spoken = lines.sentences(place.note)
49    }
50}
51
52/// The next launch in the backend's schedule that has not yet flown.
53public struct LaunchAnswer: Sendable {
54    public var launch: Launch?
55    public var rows: [AnswerRow]
56    public var spoken: String
57
58    /// Launch Library 2 statuses for launches that have left the pad.
59    static let flown: Set<String> = ["Success", "Failure", "Partial Failure", "In Flight"]
60
61    public init(launches: [Launch], at date: Date, locale: Locale = .current, timeZone: TimeZone = .current) {
62        let say = Phrasing(locale: locale, timeZone: timeZone)
63        launch = launches.filter { $0.net > date && !Self.flown.contains($0.status) }.min { $0.net < $1.net }
64        guard let l = launch else {
65            rows = []
66            spoken = "No upcoming launches are scheduled."
67            return
68        }
69        let mission = l.mission ?? l.name.components(separatedBy: " | ").last ?? l.name
70        let site = l.location.components(separatedBy: ",").first ?? l.location
71        rows = [
72            AnswerRow("Rocket", l.rocket),
73            AnswerRow("Mission", mission),
74            AnswerRow("From", site),
75            AnswerRow("Launch", "\(say.short(l.net, from: date)) · \(l.statusName)", alert: l.status == "Hold"),
76        ]
77        spoken = "\(l.rocket), \(mission), from \(site) in \(say.duration(l.net.timeIntervalSince(date), spoken: true)). Status: \(l.statusName)."
78    }
79}