Packages/MoonriseKit/Sources/Answers/FeedAnswers.swift
79 lines · 3623 bytes
3 symbols in this file
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}