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/Feeds/Sun.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/Packages/MoonriseKit/Sources/Feeds/Sun.swift history · blame · raw

133 lines · 4896 bytes

17 symbols in this file
  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}