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

Packages/MoonriseKit/Sources/Library/Scale.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/Packages/MoonriseKit/Sources/Library/Scale.swift history · blame · raw

77 lines · 3823 bytes

4 symbols in this file
 1import Foundation
 2
 3/// Something with a size, for the scale view: a body, star, nebula, galaxy or orbit.
 4public struct ScaleObject: Sendable, Identifiable, Hashable {
 5    public var name: String
 6    /// Diameter, or end-to-end length, in metres.
 7    public var meters: Double
 8    /// Library page to open, if there is one.
 9    public var page: String?
10    public var id: String { name }
11}
12
13extension Library {
14    static let lightYear = 9.4607e15
15    static let au = 1.495978707e11
16
17    /// Everything in the scale view, smallest first. Sizes come from the Library pages where they give one.
18    public static let scaleObjects: [ScaleObject] = {
19        var out: [ScaleObject] = [ScaleObject(name: "ISS", meters: 109, page: "international-space-station")]
20
21        // Sun, planets, dwarf planets and moons: the drawing's mean radius.
22        for section in [LibrarySection.sun, .planets, .dwarfPlanets, .moons] {
23            for p in pages(in: section) {
24                if let r = p.drawing?.radiusKm { out.append(ScaleObject(name: p.title, meters: 2 * r * 1000, page: p.id)) }
25            }
26        }
27
28        // Stars whose page gives a radius as "N × Sun".
29        for p in pages(in: .stars) {
30            guard let fact = p.facts.first(where: { $0.label == "Radius" }), let r = solarRadii(fact.value) else { continue }
31            out.append(ScaleObject(name: p.title, meters: 2 * r * 6.957e8, page: p.id))
32        }
33
34        // Deep-sky objects: the page's distance times its apparent size (major axis).
35        let deepSky: [(String, Double, Double)] = [ // id, light-years, arcminutes
36            ("m57", 2_000, 1.4), ("m1", 6_500, 6), ("m45", 445, 110), ("m42", 1_500, 85),
37            ("m13", 25_000, 20), ("m33", 3_000_000, 73), ("m87", 54_000_000, 7), ("m31", 2_500_000, 178),
38        ]
39        for (id, ly, arcmin) in deepSky {
40            guard let p = page(id: id) else { continue }
41            out.append(ScaleObject(name: p.title, meters: ly * lightYear * arcmin / 60 * .pi / 180, page: id))
42        }
43
44        out += [
45            ScaleObject(name: "Moon's orbit", meters: 2 * 384_400e3),
46            ScaleObject(name: "Earth's orbit", meters: 2 * au),
47            ScaleObject(name: "Neptune's orbit", meters: 2 * 30.07 * au),
48            // Voyager 1 crossed the heliopause at about 121 AU.
49            ScaleObject(name: "Heliosphere", meters: 2 * 121 * au),
50            ScaleObject(name: "Milky Way", meters: 100_000 * lightYear),
51            ScaleObject(name: "Local Group", meters: 10e6 * lightYear),
52            // Tully et al. 2014.
53            ScaleObject(name: "Laniakea Supercluster", meters: 520e6 * lightYear),
54            ScaleObject(name: "Observable universe", meters: 93e9 * lightYear),
55        ]
56        return out.sorted { $0.meters < $1.meters }
57    }()
58
59    /// 25.4 from "25.4 ± 0.2 × Sun".
60    static func solarRadii(_ s: String) -> Double? {
61        guard s.contains("× Sun"), let first = s.split(separator: " ").first else { return nil }
62        return Double(first.replacingOccurrences(of: ",", with: ""))
63    }
64
65    /// "109 m", "12,742 km", "60.1 AU", "11 light-years", "2.5 million light-years".
66    public static func formatSize(_ meters: Double) -> String {
67        func n(_ v: Double) -> String { v.formatted(.number.precision(.significantDigits(1...(v < 10 ? 2 : 3)))) }
68        switch meters {
69        case ..<1_000: return "\(Int(meters.rounded())) m"
70        case ..<1e10: return "\(Int((meters / 1000).rounded()).formatted()) km"
71        case ..<(0.1 * lightYear): return "\(n(meters / au)) AU"
72        case ..<(1e6 * lightYear): return "\(n(meters / lightYear)) light-years"
73        case ..<(1e9 * lightYear): return "\(n(meters / lightYear / 1e6)) million light-years"
74        default: return "\(n(meters / lightYear / 1e9)) billion light-years"
75        }
76    }
77}