Packages/MoonriseKit/Sources/Feeds/LiveEvent.swift
64 lines · 2525 bytes
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}