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

61 lines · 2220 bytes

 1import CoreLocation
 2import MapKit
 3import Observation
 4import SkyEngine
 5
 6/// Continuous best-accuracy fixes while an eclipse screen is open: near the edge of the path a contact moves
 7/// about 4 s per kilometre.
 8@MainActor
 9@Observable
10final class EclipseLocation: NSObject, CLLocationManagerDelegate {
11    struct Fix: Equatable {
12        var observer: Observer
13        /// Horizontal accuracy, metres.
14        var accuracy: Double
15    }
16
17    private(set) var fix: Fix?
18    private let manager = CLLocationManager()
19
20    override init() {
21        super.init()
22        manager.delegate = self
23        manager.desiredAccuracy = kCLLocationAccuracyBest
24        manager.distanceFilter = 5
25    }
26
27    func start() {
28        manager.requestWhenInUseAuthorization()
29        manager.startUpdatingLocation()
30    }
31
32    func stop() {
33        manager.stopUpdatingLocation()
34    }
35
36    nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
37        guard let l = locations.last, l.horizontalAccuracy >= 0 else { return }
38        let fix = Fix(
39            observer: Observer(latitude: l.coordinate.latitude, longitude: l.coordinate.longitude, height: l.verticalAccuracy >= 0 ? l.ellipsoidalAltitude : 0),
40            accuracy: l.horizontalAccuracy
41        )
42        Task { @MainActor in self.fix = fix }
43    }
44
45    nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
46        Task { @MainActor in log.error("Eclipse location failed: \(error.localizedDescription, privacy: .public)") }
47    }
48
49    /// The time zone at `observer` from Apple Maps, or nil offline.
50    static func timeZone(at observer: Observer) async -> TimeZone? {
51        guard let request = MKReverseGeocodingRequest(location: CLLocation(latitude: observer.latitude, longitude: observer.longitude)) else { return nil }
52        return try? await request.mapItems.first?.timeZone
53    }
54}
55
56/// "UTC+2" for a zone's offset at `date`.
57func utcOffset(_ zone: TimeZone, at date: Date) -> String {
58    let s = zone.secondsFromGMT(for: date)
59    let h = abs(s) / 3600, m = abs(s) % 3600 / 60
60    return "UTC\(s < 0 ? "−" : "+")\(h)\(m == 0 ? "" : String(format: ":%02d", m))"
61}