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

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

64 lines · 2525 bytes

7 symbols in this file
 1import Foundation
 2
 3/// A mission covered live by the backend (GET /v1/live): its phases and geocentric trajectory.
 4public struct LiveCoverage: Sendable {
 5    public struct Event: Sendable, Decodable, Hashable {
 6        public struct Phase: Sendable, Decodable, Hashable {
 7            public var at: Date
 8            public var label: String
 9        }
10
11        public var id: String
12        public var title: String
13        public var launch: Date
14        public var end: Date
15        public var phases: [Phase]
16    }
17
18    public struct Point: Sendable, Hashable {
19        public var date: Date
20        /// km, J2000 ecliptic axes, centred on Earth.
21        public var position: SIMD3<Double>
22    }
23
24    public var event: Event
25    public var at: Date
26    public var points: [Point]
27
28    /// The last phase that has started by `at`, and the next one.
29    public var phase: (current: Event.Phase?, next: Event.Phase?) {
30        (event.phases.last { $0.at <= at }, event.phases.first { $0.at > at })
31    }
32
33    /// Distance from Earth's centre at the latest point, km.
34    public var distanceKm: Double? { points.last.map { ($0.position * $0.position).sum().squareRoot() } }
35
36    private struct Response: Decodable {
37        var event: Event?
38        var at: Date?
39        var points: [[Double]]?
40    }
41
42    /// nil when no event is running.
43    public static func decode(_ data: Data) throws -> LiveCoverage? {
44        let d = JSONDecoder()
45        d.dateDecodingStrategy = .custom { decoder in
46            let s = try decoder.singleValueContainer().decode(String.self)
47            if let date = try? Date(s, strategy: .iso8601) { return date }
48            return try Date(s, strategy: Date.ISO8601FormatStyle(includingFractionalSeconds: true))
49        }
50        let r = try d.decode(Response.self, from: data)
51        guard let event = r.event, let at = r.at else { return nil }
52        let points = (r.points ?? []).filter { $0.count == 4 }.map {
53            Point(date: Date(timeIntervalSince1970: $0[0]), position: SIMD3($0[1], $0[2], $0[3]))
54        }
55        return LiveCoverage(event: event, at: at, points: points)
56    }
57
58    /// `at` replays a past event (testing); nil means now.
59    public static func fetch(base: URL, at: Date? = nil, session: URLSession = .shared) async throws -> LiveCoverage? {
60        var url = base.appending(path: "v1/live")
61        if let at { url.append(queryItems: [URLQueryItem(name: "at", value: at.ISO8601Format())]) }
62        return try decode(await session.data(from: url).0)
63    }
64}