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}