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

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/App/Sources/MarsSection.swift history · blame · raw

90 lines · 4271 bytes

3 symbols in this file
 1import Feeds
 2import SkyEngine
 3import SwiftUI
 4
 5/// Local mean solar time and sol number at the active Mars rovers, ticking each second, and their latest raw images.
 6struct MarsSection: View {
 7    @State private var images: [MarsImage] = []
 8    @State private var shown: MarsImage?
 9
10    var body: some View {
11        VStack(alignment: .leading, spacing: 10) {
12            Text("Mars now").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
13            Text("The time of day where NASA's two rovers are working, and their newest raw photos, usually a few hours old.")
14                .font(.footnote).dimmed(0.75)
15            VStack(alignment: .leading, spacing: 8) {
16                ForEach(MarsTime.Rover.active) { rover in
17                    TimelineView(.periodic(from: .now, by: 1)) { ctx in
18                        HStack(alignment: .firstTextBaseline) {
19                            VStack(alignment: .leading, spacing: 2) {
20                                Text(rover.name).font(.subheadline.weight(.medium))
21                                Text("\(rover.site) · sol \(MarsTime.sol(ctx.date, rover: rover))").font(.footnote).dimmed(0.7)
22                            }
23                            Spacer()
24                            Text(MarsTime.format(hours: MarsTime.localMeanTime(ctx.date, eastLongitude: rover.longitude)))
25                                .font(.title3.monospacedDigit())
26                        }
27                        .accessibilityElement(children: .combine)
28                    }
29                    strip(rover.name)
30                }
31            }
32            Text("Local mean solar time. A Mars sol is 24 h 39 m 35 s. Raw images: NASA/JPL-Caltech.").font(.caption2).dimmed(0.5)
33        }
34        .task {
35            // The rovers send a few batches a day; reuse a fetch from the last 30 minutes.
36            if let cached: [MarsImage] = FeedMemo.get("mars", maxAge: 1800) { images = cached; return }
37            if let fresh = try? await MarsImage.latest() { images = fresh; FeedMemo.set("mars", fresh) }
38        }
39        .sheet(item: $shown) { MarsImageView(image: $0) }
40    }
41
42    @ViewBuilder
43    private func strip(_ rover: String) -> some View {
44        let mine = images.filter { $0.rover == rover }
45        if !mine.isEmpty {
46            ScrollView(.horizontal, showsIndicators: false) {
47                HStack(spacing: 6) {
48                    ForEach(mine) { image in
49                        Button { shown = image } label: {
50                            RemoteImage(url: image.thumbnail, points: 130) { Color.white.opacity(0.06) }
51                            .frame(width: 88, height: 88)
52                            .clipped()
53                        }
54                        .accessibilityLabel("\(image.camera), sol \(image.sol)")
55                    }
56                }
57            }
58            .padding(.bottom, 6)
59        }
60    }
61}
62
63struct MarsImageView: View {
64    var image: MarsImage
65    @Environment(\.dismiss) private var dismiss
66
67    var body: some View {
68        NavigationStack {
69            ScrollView {
70                VStack(alignment: .leading, spacing: 8) {
71                    AsyncImage(url: image.image) { phase in
72                        switch phase {
73                        case .success(let img): img.resizable().scaledToFit()
74                        case .failure: Color.black.frame(height: 200).overlay(Text("Image unavailable").font(.caption).dimmed(0.6))
75                        default: Color.black.frame(height: 300).overlay(ProgressView())
76                        }
77                    }
78                    Text("\(image.rover) · \(image.camera) · sol \(image.sol)").font(.subheadline.weight(.medium))
79                    Text(image.taken.formatted(date: .abbreviated, time: .shortened)).font(.footnote).dimmed(0.7)
80                    Text("Raw, unprocessed image. Credit: \(image.credit).").font(.caption2).dimmed(0.55)
81                }
82                .padding(.horizontal, 16)
83            }
84            .background(Color.black.ignoresSafeArea())
85            .navigationBarTitleDisplayMode(.inline)
86            .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } }
87        }
88        .preferredColorScheme(.dark)
89    }
90}