Packages/MoonriseKit/Sources/Feeds/Sun.swift
133 lines · 4896 bytes
1import Foundation
2
3/// Latest full-disk images from NASA's Solar Dynamics Observatory.
4/// Credit: "Courtesy of NASA/SDO and the AIA, EVE, and HMI science teams."
5public enum SolarImage: String, CaseIterable, Sendable, Identifiable {
6 case corona = "0171"
7 case hotCorona = "0193"
8 case chromosphere = "0304"
9 case visible = "HMIIC"
10
11 public var id: String { rawValue }
12
13 public var name: String {
14 switch self {
15 case .corona: "171 Å"
16 case .hotCorona: "193 Å"
17 case .chromosphere: "304 Å"
18 case .visible: "Visible"
19 }
20 }
21
22 public var caption: String {
23 switch self {
24 case .corona: "Quiet corona and upper transition region, about 600,000 K. Shows coronal loops."
25 case .hotCorona: "Corona at about 1 million K and hot flare plasma. Coronal holes look dark."
26 case .chromosphere: "Chromosphere and transition region, about 50,000 K. Shows filaments and prominences."
27 case .visible: "The photosphere in visible light, where sunspots appear."
28 }
29 }
30
31 /// Sizes SDO publishes: 170, 256, 512, 1024, 2048, 3072, 4096.
32 public func url(size: Int = 1024) -> URL {
33 URL(string: "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_\(size)_\(rawValue).jpg")!
34 }
35
36 public static let credit = "Courtesy of NASA/SDO and the AIA, EVE, and HMI science teams."
37}
38
39/// Three-hour planetary Kp, observed and predicted (SWPC 3-day forecast product).
40public struct KpBar: Sendable, Equatable, Identifiable {
41 public var start: Date
42 public var kp: Double
43 public var predicted: Bool
44 public var id: Date { start }
45}
46
47extension SpaceWeather {
48 public static func decodeKpBars(_ data: Data) throws -> [KpBar] {
49 try JSONDecoder().decode([Forecast].self, from: data).compactMap { row in
50 parse(row.time_tag).map { KpBar(start: $0, kp: row.kp, predicted: row.observed != "observed") }
51 }
52 }
53
54 public static func fetchKpBars(session: URLSession = .shared) async throws -> [KpBar] {
55 try decodeKpBars(await session.data(from: forecastURL).0)
56 }
57}
58
59/// GOES long-channel (0.1–0.8 nm) X-ray flux, which defines flare classes.
60public struct XRaySample: Sendable, Equatable, Identifiable {
61 public var time: Date
62 /// W/m².
63 public var flux: Double
64 public var id: Date { time }
65}
66
67public enum XRay {
68 public static let dayURL = URL(string: "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json")!
69
70 struct Row: Decodable {
71 var time_tag: String
72 var flux: Double?
73 var energy: String
74 }
75
76 public static func decode(_ data: Data) throws -> [XRaySample] {
77 try JSONDecoder().decode([Row].self, from: data).compactMap { row in
78 guard row.energy == "0.1-0.8nm", let flux = row.flux, flux > 0,
79 let time = try? Date(row.time_tag, strategy: .iso8601) else { return nil }
80 return XRaySample(time: time, flux: flux)
81 }
82 }
83
84 public static func fetch(session: URLSession = .shared) async throws -> [XRaySample] {
85 try decode(await session.data(from: dayURL).0)
86 }
87
88 /// Flare class for a flux, e.g. 1.5e-6 → "C1.5".
89 public static func flareClass(_ flux: Double) -> String {
90 let classes: [(String, Double)] = [("X", 1e-4), ("M", 1e-5), ("C", 1e-6), ("B", 1e-7), ("A", 1e-8)]
91 for (letter, base) in classes where flux >= base {
92 return String(format: "%@%.1f", letter, flux / base)
93 }
94 return String(format: "A%.1f", flux / 1e-8)
95 }
96}
97
98/// NOAA SWPC OVATION aurora model: probability of visible aurora (0–100) on a 1° grid.
99public struct Ovation: Sendable {
100 public struct Cell: Sendable, Equatable {
101 public var lon: Double
102 public var lat: Double
103 public var probability: Double
104 }
105
106 public var forecastTime: Date
107 public var cells: [Cell]
108
109 public static let url = URL(string: "https://services.swpc.noaa.gov/json/ovation_aurora_latest.json")!
110
111 struct Raw: Decodable {
112 var forecastTime: String
113 var coordinates: [[Double]]
114 enum CodingKeys: String, CodingKey {
115 case forecastTime = "Forecast Time"
116 case coordinates
117 }
118 }
119
120 /// Keeps cells with a non-zero probability.
121 public static func decode(_ data: Data) throws -> Ovation {
122 let raw = try JSONDecoder().decode(Raw.self, from: data)
123 let cells = raw.coordinates.compactMap { c -> Cell? in
124 guard c.count == 3, c[2] > 0 else { return nil }
125 return Cell(lon: c[0], lat: c[1], probability: c[2])
126 }
127 return Ovation(forecastTime: (try? Date(raw.forecastTime, strategy: .iso8601)) ?? .now, cells: cells)
128 }
129
130 public static func fetch(session: URLSession = .shared) async throws -> Ovation {
131 try decode(await session.data(from: url).0)
132 }
133}