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

6c4b28384d65e09486e0ad62f2ac9d10401cc80c
moonrise/App/Sources/EclipseLocation.swift history · blame · raw

67 lines · 2529 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    /// Screens using the fixes; updates stop when the last one goes.
20    private var users = 0
21
22    override init() {
23        super.init()
24        manager.delegate = self
25        manager.desiredAccuracy = kCLLocationAccuracyBest
26        manager.distanceFilter = 5
27    }
28
29    func start() {
30        users += 1
31        guard users == 1 else { return }
32        manager.requestWhenInUseAuthorization()
33        manager.startUpdatingLocation()
34    }
35
36    func stop() {
37        users -= 1
38        if users == 0 { manager.stopUpdatingLocation() }
39    }
40
41    nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
42        guard let l = locations.last, l.horizontalAccuracy >= 0 else { return }
43        let fix = Fix(
44            observer: Observer(latitude: l.coordinate.latitude, longitude: l.coordinate.longitude, height: l.verticalAccuracy >= 0 ? l.ellipsoidalAltitude : 0),
45            accuracy: l.horizontalAccuracy
46        )
47        Task { @MainActor in self.fix = fix }
48    }
49
50    nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
51        Task { @MainActor in log.error("Eclipse location failed: \(error.localizedDescription, privacy: .public)") }
52    }
53
54    /// The time zone and town at `observer` from Apple Maps, or nil offline.
55    static func site(at observer: Observer) async -> (zone: TimeZone, city: String?)? {
56        guard let request = MKReverseGeocodingRequest(location: CLLocation(latitude: observer.latitude, longitude: observer.longitude)),
57              let item = try? await request.mapItems.first, let zone = item.timeZone else { return nil }
58        return (zone, item.addressRepresentations?.cityName)
59    }
60}
61
62/// "UTC+2" for a zone's offset at `date`.
63func utcOffset(_ zone: TimeZone, at date: Date) -> String {
64    let s = zone.secondsFromGMT(for: date)
65    let h = abs(s) / 3600, m = abs(s) % 3600 / 60
66    return "UTC\(s < 0 ? "−" : "+")\(h)\(m == 0 ? "" : String(format: ":%02d", m))"
67}