Packages/MoonriseKit/Sources/SkyRenderer/SkyScene.swift
157 lines · 6109 bytes
12 symbols in this file
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
46/// A Messier object from OpenNGC (CC BY-SA 4.0).
47public struct DeepSkyObject: Sendable, Decodable {
48 public enum Kind: Sendable { case galaxy, openCluster, globularCluster, nebula, planetaryNebula }
49
50 public var messier: Int
51 /// OpenNGC type code, e.g. "G", "OCl", "GCl", "PN", "Neb".
52 public var type: String
53 /// J2000, degrees.
54 public var ra: Double
55 public var dec: Double
56 public var magnitude: Double?
57 /// Arcminutes.
58 public var majorAxis: Double?
59 public var minorAxis: Double?
60 /// Degrees east of north.
61 public var positionAngle: Double?
62 /// Library page id.
63 public var page: String
64
65 public var kind: Kind {
66 switch type {
67 case "G": .galaxy
68 case "GCl": .globularCluster
69 case "PN": .planetaryNebula
70 case "Neb", "HII", "RfN", "Cl+N", "SNR": .nebula
71 // OCl, and the star cloud M24, double star M40 and asterism M73.
72 default: .openCluster
73 }
74 }
75}
76
77public enum Catalog {
78 /// Yale Bright Star Catalogue, 5th ed. (VizieR V/50): HR, RA and Dec (J2000, degrees), V, B−V,
79 /// proper motion in RA (× cos dec) and Dec (arcsec/yr).
80 public static let stars: [Star] = {
81 let url = Bundle.module.url(forResource: "bsc5", withExtension: "json")!
82 let rows = try! JSONDecoder().decode([[Double]].self, from: Data(contentsOf: url))
83 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]) }
84 }()
85
86 /// Stars by HR number.
87 public static let starsByHR: [Int: Star] = Dictionary(uniqueKeysWithValues: stars.map { ($0.hr, $0) })
88
89 /// Built by scripts/make-skycultures.py.
90 public static let skyCultures: [SkyCulture] = {
91 let url = Bundle.module.url(forResource: "skycultures", withExtension: "json")!
92 return try! JSONDecoder().decode([SkyCulture].self, from: Data(contentsOf: url))
93 }()
94
95 public static let constellations: [Constellation] = {
96 let url = Bundle.module.url(forResource: "constellations", withExtension: "json")!
97 return try! JSONDecoder().decode([Constellation].self, from: Data(contentsOf: url))
98 }()
99
100 /// Built by scripts/make-messier.py.
101 public static let deepSky: [DeepSkyObject] = {
102 let url = Bundle.module.url(forResource: "messier", withExtension: "json")!
103 return try! JSONDecoder().decode([DeepSkyObject].self, from: Data(contentsOf: url))
104 }()
105}
106
107public struct PlacedStar: Sendable {
108 public var star: Star
109 public var position: Horizontal
110}
111
112public struct PlacedDeepSky: Sendable {
113 public var object: DeepSkyObject
114 public var position: Horizontal
115 /// A point 0.1° north (J2000), for the symbol's scale and orientation on screen.
116 public var north: Horizontal
117}
118
119public struct PlacedBody: Sendable {
120 public var body: Body
121 public var position: Horizontal
122 public var magnitude: Double
123 public var phaseFraction: Double
124}
125
126/// Everything above (and just below) the horizon for one place and time.
127public struct SkyScene: Sendable {
128 public var date: Date
129 public var observer: Observer
130 public var stars: [PlacedStar]
131 public var starsByHR: [Int: Horizontal]
132 public var bodies: [PlacedBody]
133 public var deepSky: [PlacedDeepSky]
134 /// Moon's elongation east of the sun, degrees (0 new, 180 full).
135 public var moonPhase: Double
136
137 public init(date: Date, observer: Observer, magnitudeLimit: Double = 6.5) throws {
138 self.date = date
139 self.observer = observer
140 let visible = Catalog.stars.filter { $0.magnitude <= magnitudeLimit }
141 let positions = SkyEngine.horizontal(j2000: visible.map { $0.position(at: date) }, at: date, from: observer)
142 stars = zip(visible, positions).map { PlacedStar(star: $0, position: $1) }
143 starsByHR = Dictionary(uniqueKeysWithValues: stars.map { ($0.star.hr, $0.position) })
144 let dso = SkyEngine.horizontal(j2000: Catalog.deepSky.flatMap { [($0.ra, $0.dec), ($0.ra, $0.dec + 0.1)] }, at: date, from: observer)
145 deepSky = Catalog.deepSky.enumerated().map { i, o in PlacedDeepSky(object: o, position: dso[2 * i], north: dso[2 * i + 1]) }
146 bodies = try ([.moon] + Body.planets).map { body in
147 let i = try SkyEngine.illumination(body, at: date)
148 return PlacedBody(
149 body: body,
150 position: try SkyEngine.horizontal(body, at: date, from: observer),
151 magnitude: i.magnitude,
152 phaseFraction: i.phaseFraction
153 )
154 }
155 moonPhase = try SkyEngine.moonPhase(at: date)
156 }
157}