v2: Mars local time !25
merged
merged by cmc on 2026-10-18 08:47 UTC
· krz/moonrise:v2-mars-time into main
4 files changed, +98 −0
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 | 90 | DSNSection(ink: ink, accent: accent).padding(.top, 24) |
| 91 | MarsSection().padding(.top, 24) |
| 91 | 92 | Text("Spacecraft positions: NASA/JPL Horizons. Pioneer 10 and 11 positions after contact was lost are predictions.") |
| 92 | 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 | } |