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/SkyRenderer/SkyScene.swift

59d401dca1a327705632d1e2e20f00f807dee828
moonrise/Packages/MoonriseKit/Sources/SkyRenderer/SkyScene.swift history · blame · raw

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}