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/PolarMap.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/Packages/MoonriseKit/Sources/SkyRenderer/PolarMap.swift history · blame · raw

102 lines · 4199 bytes

5 symbols in this file
  1import Foundation
  2import SwiftUI
  3
  4/// Natural Earth 1:110m coastlines (public domain), polylines of [longitude, latitude],
  5/// kept where they reach 30° or more from the equator.
  6public enum Coastline {
  7    public static let lines: [[(lon: Double, lat: Double)]] = {
  8        let url = Bundle.module.url(forResource: "coastline", withExtension: "json")!
  9        let raw = try! JSONDecoder().decode([[[Double]]].self, from: Data(contentsOf: url))
 10        return raw.map { $0.map { (lon: $0[0], lat: $0[1]) } }
 11    }()
 12}
 13
 14/// Azimuthal equidistant view of one polar region, rotated so the given
 15/// longitude points down the screen.
 16public struct PolarProjection: Sendable {
 17    public var north: Bool
 18    public var centerLongitude: Double
 19    /// Latitude at the edge of the map (absolute).
 20    public var edgeLatitude: Double
 21    public var size: CGSize
 22
 23    public init(north: Bool, centerLongitude: Double, edgeLatitude: Double = 40, size: CGSize) {
 24        self.north = north
 25        self.centerLongitude = centerLongitude
 26        self.edgeLatitude = edgeLatitude
 27        self.size = size
 28    }
 29
 30    public func point(lon: Double, lat: Double) -> CGPoint? {
 31        let colat = 90 - (north ? lat : -lat)
 32        guard colat <= 90 - edgeLatitude + 2 else { return nil }
 33        let scale = min(size.width, size.height) / 2 / (90 - edgeLatitude)
 34        let a = (lon - centerLongitude) * .pi / 180 * (north ? 1 : -1)
 35        let r = colat * scale
 36        return CGPoint(x: size.width / 2 + r * sin(a), y: size.height / 2 + r * cos(a))
 37    }
 38}
 39
 40/// A gridded value (e.g. aurora probability, 0–100) on 1° cells.
 41public struct GridValue: Sendable {
 42    public var lon: Double
 43    public var lat: Double
 44    public var value: Double
 45
 46    public init(lon: Double, lat: Double, value: Double) {
 47        self.lon = lon
 48        self.lat = lat
 49        self.value = value
 50    }
 51}
 52
 53public struct AuroraMap: View {
 54    public var cells: [GridValue]
 55    public var observer: (lat: Double, lon: Double)
 56    public var ink: Color
 57    public var aurora: Color
 58
 59    public init(cells: [GridValue], observer: (lat: Double, lon: Double), ink: Color, aurora: Color = Color(red: 0.35, green: 0.85, blue: 0.5)) {
 60        self.cells = cells
 61        self.observer = observer
 62        self.ink = ink
 63        self.aurora = aurora
 64    }
 65
 66    public var body: some View {
 67        Canvas { context, size in
 68            let north = observer.lat >= 0
 69            let p = PolarProjection(north: north, centerLongitude: observer.lon, size: size)
 70            let scale = min(size.width, size.height) / 2 / (90 - p.edgeLatitude)
 71
 72            for c in cells where (c.lat >= 0) == north && c.value >= 3 {
 73                guard let pt = p.point(lon: c.lon, lat: c.lat) else { continue }
 74                let s = max(2, scale * 1.1)
 75                context.fill(Path(CGRect(x: pt.x - s / 2, y: pt.y - s / 2, width: s, height: s)),
 76                             with: .color(aurora.opacity(min(0.9, 0.15 + c.value / 40))))
 77            }
 78
 79            for lat in stride(from: 50.0, through: 80, by: 10) {
 80                let r = (90 - lat) * scale
 81                context.stroke(Path(ellipseIn: CGRect(x: size.width / 2 - r, y: size.height / 2 - r, width: 2 * r, height: 2 * r)),
 82                               with: .color(ink.opacity(0.15)), style: StrokeStyle(lineWidth: 0.5, dash: [2, 3]))
 83            }
 84
 85            var coast = Path()
 86            for line in Coastline.lines {
 87                var pen = false
 88                for c in line {
 89                    guard let pt = p.point(lon: c.lon, lat: c.lat) else { pen = false; continue }
 90                    if pen { coast.addLine(to: pt) } else { coast.move(to: pt); pen = true }
 91                }
 92            }
 93            context.stroke(coast, with: .color(ink.opacity(0.6)), lineWidth: 0.7)
 94
 95            if let you = p.point(lon: observer.lon, lat: observer.lat) {
 96                context.fill(Path(ellipseIn: CGRect(x: you.x - 4, y: you.y - 4, width: 8, height: 8)), with: .color(ink))
 97                context.draw(Text("You").font(.caption2).foregroundStyle(ink), at: CGPoint(x: you.x, y: you.y + 12))
 98            }
 99        }
100        .aspectRatio(1, contentMode: .fit)
101    }
102}