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

Commit 9f74997e35

9f74997e35fbbc046bba64a7098c8777789f6418

parent: 093836509d

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-18 08:47 UTC

Add Mars local time and sol numbers for Perseverance and Curiosity

- SkyEngine.MarsTime: Mars Sol Date and local mean solar time after
  Mars24 (Allison & McEwen 2000); mission sol from each rover's landing
  sol (Perseverance 52304, Curiosity 49269). Tests: MSD 44796.0 at
  2000-01-06 TT, landing days are sol 0, one sol later is sol 1.
- System tab: Mars now, ticking each second.

Layout: unified · split

App/Sources/MarsSection.swift added +28
@@ -0,0 +1,28 @@
1import SkyEngine
2import SwiftUI
3
4/// Local mean solar time and sol number at the active Mars rovers, ticking each second.
5struct MarsSection: View {
6 var body: some View {
7 VStack(alignment: .leading, spacing: 10) {
8 Text("Mars now").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
9 TimelineView(.periodic(from: .now, by: 1)) { ctx in
10 VStack(alignment: .leading, spacing: 8) {
11 ForEach(MarsTime.Rover.active) { rover in
12 HStack(alignment: .firstTextBaseline) {
13 VStack(alignment: .leading, spacing: 2) {
14 Text(rover.name).font(.subheadline.weight(.medium))
15 Text("\(rover.site) · sol \(MarsTime.sol(ctx.date, rover: rover))").font(.footnote).dimmed(0.7)
16 }
17 Spacer()
18 Text(MarsTime.format(hours: MarsTime.localMeanTime(ctx.date, eastLongitude: rover.longitude)))
19 .font(.title3.monospacedDigit())
20 }
21 .accessibilityElement(children: .combine)
22 }
23 }
24 }
25 Text("Local mean solar time. A Mars sol is 24 h 39 m 35 s.").font(.caption2).dimmed(0.5)
26 }
27 }
28}
App/Sources/SystemScreen.swift +1
@@ -88,6 +88,7 @@ struct SystemScreen: View {
88 } 88 }
89 } 89 }
90 DSNSection(ink: ink, accent: accent).padding(.top, 24) 90 DSNSection(ink: ink, accent: accent).padding(.top, 24)
91 MarsSection().padding(.top, 24)
91 Text("Spacecraft positions: NASA/JPL Horizons. Pioneer 10 and 11 positions after contact was lost are predictions.") 92 Text("Spacecraft positions: NASA/JPL Horizons. Pioneer 10 and 11 positions after contact was lost are predictions.")
92 .font(.caption2).dimmed(0.5).padding(.vertical, 16) 93 .font(.caption2).dimmed(0.5).padding(.vertical, 16)
93 } 94 }
Packages/MoonriseKit/Sources/SkyEngine/MarsTime.swift added +45
@@ -0,0 +1,45 @@
1import Foundation
2
3/// Mars Coordinated Time and rover local times, after NASA GISS Mars24 (Allison & McEwen 2000).
4public enum MarsTime {
5 public struct Rover: Sendable, Identifiable {
6 public var name: String
7 public var site: String
8 /// East longitude in degrees.
9 public var longitude: Double
10 /// Mars Sol Date of the landing sol's local midnight (sol 0).
11 public var landingSol: Int
12 public var id: String { name }
13
14 public static let active: [Rover] = [
15 Rover(name: "Perseverance", site: "Jezero Crater", longitude: 77.45, landingSol: 52304),
16 Rover(name: "Curiosity", site: "Gale Crater", longitude: 137.44, landingSol: 49269),
17 ]
18 }
19
20 /// TT − UTC: 32.184 s plus 37 leap seconds (none since 2017).
21 static let ttMinusUTC = 69.184
22
23 /// Mars Sol Date.
24 public static func msd(_ date: Date) -> Double {
25 let jdTT = (date.timeIntervalSince1970 + ttMinusUTC) / 86_400 + 2_440_587.5
26 return (jdTT - 2_405_522.0028779) / 1.0274912517
27 }
28
29 /// Local mean solar time in hours (0..<24) at an east longitude.
30 public static func localMeanTime(_ date: Date, eastLongitude: Double) -> Double {
31 let mtc = (msd(date) * 24).truncatingRemainder(dividingBy: 24)
32 let t = (mtc + eastLongitude / 15).truncatingRemainder(dividingBy: 24)
33 return t < 0 ? t + 24 : t
34 }
35
36 /// Mission sol number (sol 0 is the landing sol).
37 public static func sol(_ date: Date, rover: Rover) -> Int {
38 Int((msd(date) + rover.longitude / 360).rounded(.down)) - rover.landingSol
39 }
40
41 public static func format(hours: Double) -> String {
42 let total = Int(hours * 60)
43 return String(format: "%02d:%02d", total / 60 % 24, total % 60)
44 }
45}
Packages/MoonriseKit/Tests/SkyEngineTests/MarsTimeTests.swift added +24
@@ -0,0 +1,24 @@
1import Foundation
2import Testing
3@testable import SkyEngine
4
5@Test func msdReferenceEpoch() {
6 // Allison & McEwen (2000): MSD 44796.0 at about 2000-01-06 00:00 TT.
7 let tt = Date(timeIntervalSince1970: 947_116_800 - 69.184)
8 #expect(abs(MarsTime.msd(tt) - 44796.0) < 0.001)
9}
10
11@Test func landingDaysAreSolZero() throws {
12 let perseverance = try #require(MarsTime.Rover.active.first { $0.name == "Perseverance" })
13 let curiosity = try #require(MarsTime.Rover.active.first { $0.name == "Curiosity" })
14 #expect(MarsTime.sol(utc("2021-02-18T20:55:00Z"), rover: perseverance) == 0)
15 #expect(MarsTime.sol(utc("2012-08-06T05:17:57Z"), rover: curiosity) == 0)
16 // One sol is 88,775.244 s.
17 #expect(MarsTime.sol(utc("2021-02-18T20:55:00Z").addingTimeInterval(88_775.244), rover: perseverance) == 1)
18}
19
20@Test func localTimeIsWithinADay() {
21 let t = MarsTime.localMeanTime(.now, eastLongitude: 77.45)
22 #expect(t >= 0 && t < 24)
23 #expect(MarsTime.format(hours: 13.5) == "13:30")
24}