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

App/Sources/DSNSection.swift

main
moonrise/App/Sources/DSNSection.swift history · blame · raw

73 lines · 3541 bytes

4 symbols in this file
 1import Feeds
 2import SwiftUI
 3
 4/// Which Deep Space Network dishes are talking to which spacecraft right now.
 5struct DSNSection: View {
 6    var ink: Color
 7    var accent: Color
 8    @State private var status: DSNStatus?
 9    @State private var failed = false
10
11    private var activeDishes: [DSNStatus.Dish] {
12        status?.dishes.filter { !$0.links.isEmpty } ?? []
13    }
14
15    var body: some View {
16        VStack(alignment: .leading, spacing: 10) {
17            Text("Deep Space Network now").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
18            Text("NASA talks to its deep-space probes through giant dishes at three sites spaced around the world, so one always faces any spacecraft. This is who each site is talking to now.")
19                .font(.footnote).dimmed(0.75)
20            if let status {
21                ForEach(["Goldstone", "Madrid", "Canberra"], id: \.self) { station in
22                    let dishes = activeDishes.filter { $0.station == station }
23                    VStack(alignment: .leading, spacing: 4) {
24                        Text(station).font(.subheadline.weight(.medium))
25                        if dishes.isEmpty {
26                            Text("No spacecraft in contact").font(.footnote).dimmed(0.6)
27                        }
28                        ForEach(dishes) { dish in
29                            ForEach(dish.links, id: \.self) { link in
30                                HStack(alignment: .firstTextBaseline) {
31                                    Text(link.spacecraft).font(.footnote)
32                                    Spacer()
33                                    Text(describe(link, dish: dish)).font(.footnote.monospacedDigit()).dimmed(0.7)
34                                }
35                                .accessibilityElement(children: .combine)
36                            }
37                        }
38                    }
39                    .padding(.vertical, 4)
40                }
41                Text("Updated \(status.updated.formatted(date: .omitted, time: .shortened)). Data: NASA/JPL Deep Space Network.")
42                    .font(.caption2).dimmed(0.5)
43            } else if failed {
44                Text("Couldn't load Deep Space Network status.").font(.footnote).foregroundStyle(accent)
45            } else {
46                ProgressView().tint(ink)
47            }
48        }
49        .task {
50            while !Task.isCancelled {
51                do { status = try await DSNStatus.fetch(); failed = false } catch { failed = status == nil }
52                try? await Task.sleep(for: .seconds(30))
53            }
54        }
55    }
56
57    private func describe(_ l: DSNStatus.Link, dish: DSNStatus.Dish) -> String {
58        var parts = [dish.name.replacingOccurrences(of: "DSS", with: "DSS ")]
59        if l.receiving && l.sending { parts.append("two-way") } else if l.receiving { parts.append("receiving") } else { parts.append("sending") }
60        if let r = l.downlinkRate { parts.append(rate(r)) }
61        if let rt = l.roundTrip, rt > 60 { parts.append("round trip \(duration(rt))") }
62        return parts.joined(separator: " · ")
63    }
64
65    private func rate(_ bps: Double) -> String {
66        bps >= 1_000_000 ? String(format: "%.1f Mb/s", bps / 1_000_000) : bps >= 1000 ? String(format: "%.0f kb/s", bps / 1000) : String(format: "%.0f b/s", bps)
67    }
68
69    private func duration(_ s: Double) -> String {
70        let h = Int(s / 3600), m = Int(s.truncatingRemainder(dividingBy: 3600) / 60)
71        return h > 0 ? "\(h) h \(m) m" : "\(m) m"
72    }
73}