Packages/MoonriseKit/Sources/SkyRenderer/SkyScene.swift
110 lines · 4305 bytes
1import Foundation
2import SkyEngine
3
4public struct Star: Sendable {
5 public var hr: Int
6 public var ra: Double
7 public var dec: Double
8 public var magnitude: Double
9 public var colorIndex: Double
10 /// Proper motion in arcseconds per year; `pmRA` already includes cos(dec).
11 public var pmRA: Double = 0
12 public var pmDec: Double = 0
13
14 /// J2000 coordinates moved by proper motion to `date` (linear, which is close enough over a few thousand years).
15 public func position(at date: Date) -> (ra: Double, dec: Double) {
16 let years = date.timeIntervalSince(SkyEngine.j2000) / (365.25 * 86_400)
17 let dec = self.dec + pmDec * years / 3600
18 let ra = self.ra + pmRA * years / 3600 / max(cos(self.dec * .pi / 180), 1e-6)
19 return (ra, dec)
20 }
21}
22
23public struct Constellation: Sendable, Decodable {
24 public var id: String
25 /// Polylines of HR numbers.
26 public var lines: [[Int]]
27}
28
29/// Another culture's figures over the same stars, from Stellarium's sky cultures (CC BY-SA).
30public struct SkyCulture: Sendable, Decodable, Identifiable {
31 public struct Figure: Sendable, Decodable {
32 public var name: String
33 public var native: String?
34 /// Polylines of HR numbers.
35 public var lines: [[Int]]
36 }
37
38 public var id: String
39 public var name: String
40 public var region: String
41 public var licence: String
42 public var source: URL
43 public var constellations: [Figure]
44}
45
46public enum Catalog {
47 /// Yale Bright Star Catalogue, 5th ed. (VizieR V/50): HR, RA and Dec (J2000, degrees), V, B−V,
48 /// proper motion in RA (× cos dec) and Dec (arcsec/yr).
49 public static let stars: [Star] = {
50 let url = Bundle.module.url(forResource: "bsc5", withExtension: "json")!
51 let rows = try! JSONDecoder().decode([[Double]].self, from: Data(contentsOf: url))
52 return rows.map { Star(hr: Int($0[0]), ra: $0[1], dec: $0[2], magnitude: $0[3], colorIndex: $0[4], pmRA: $0[5], pmDec: $0[6]) }
53 }()
54
55 /// Stars by HR number.
56 public static let starsByHR: [Int: Star] = Dictionary(uniqueKeysWithValues: stars.map { ($0.hr, $0) })
57
58 /// Built by scripts/make-skycultures.py.
59 public static let skyCultures: [SkyCulture] = {
60 let url = Bundle.module.url(forResource: "skycultures", withExtension: "json")!
61 return try! JSONDecoder().decode([SkyCulture].self, from: Data(contentsOf: url))
62 }()
63
64 public static let constellations: [Constellation] = {
65 let url = Bundle.module.url(forResource: "constellations", withExtension: "json")!
66 return try! JSONDecoder().decode([Constellation].self, from: Data(contentsOf: url))
67 }()
68}
69
70public struct PlacedStar: Sendable {
71 public var star: Star
72 public var position: Horizontal
73}
74
75public struct PlacedBody: Sendable {
76 public var body: Body
77 public var position: Horizontal
78 public var magnitude: Double
79 public var phaseFraction: Double
80}
81
82/// Everything above (and just below) the horizon for one place and time.
83public struct SkyScene: Sendable {
84 public var date: Date
85 public var observer: Observer
86 public var stars: [PlacedStar]
87 public var starsByHR: [Int: Horizontal]
88 public var bodies: [PlacedBody]
89 /// Moon's elongation east of the sun, degrees (0 new, 180 full).
90 public var moonPhase: Double
91
92 public init(date: Date, observer: Observer, magnitudeLimit: Double = 6.5) throws {
93 self.date = date
94 self.observer = observer
95 let visible = Catalog.stars.filter { $0.magnitude <= magnitudeLimit }
96 let positions = SkyEngine.horizontal(j2000: visible.map { $0.position(at: date) }, at: date, from: observer)
97 stars = zip(visible, positions).map { PlacedStar(star: $0, position: $1) }
98 starsByHR = Dictionary(uniqueKeysWithValues: stars.map { ($0.star.hr, $0.position) })
99 bodies = try ([.moon] + Body.planets).map { body in
100 let i = try SkyEngine.illumination(body, at: date)
101 return PlacedBody(
102 body: body,
103 position: try SkyEngine.horizontal(body, at: date, from: observer),
104 magnitude: i.magnitude,
105 phaseFraction: i.phaseFraction
106 )
107 }
108 moonPhase = try SkyEngine.moonPhase(at: date)
109 }
110}