Packages/MoonriseKit/Sources/Library/Scale.swift
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}