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 @@ |
| |
1 | import SkyEngine |
| |
2 | import SwiftUI |
| |
3 | |
| |
4 | /// Local mean solar time and sol number at the active Mars rovers, ticking each second. |
| |
5 | struct 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 @@ |
| |
1 | import Foundation |
| |
2 | |
| |
3 | /// Mars Coordinated Time and rover local times, after NASA GISS Mars24 (Allison & McEwen 2000). |
| |
4 | public 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 @@ |
| |
1 | import Foundation |
| |
2 | import 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 | } |