93 lines
3.3 KiB
Swift
93 lines
3.3 KiB
Swift
import CoreLocation
|
|
import Foundation
|
|
|
|
struct SunPosition {
|
|
let azimuth: Double // 0...360, degrees clockwise from true north
|
|
let elevation: Double // -90...90 degrees above horizon
|
|
}
|
|
|
|
enum SunMath {
|
|
static func calculateSunPosition(date: Date, coordinate: CLLocationCoordinate2D) -> SunPosition {
|
|
let timezone = TimeZone.current
|
|
let localOffsetHours = Double(timezone.secondsFromGMT(for: date)) / 3600.0
|
|
let julianDay = date.julianDay
|
|
|
|
let n = julianDay - 2_451_545.0
|
|
let meanLongitude = normalizeDegrees(280.46 + 0.985_647_4 * n)
|
|
let meanAnomaly = normalizeDegrees(357.528 + 0.985_600_3 * n)
|
|
|
|
let lambda = meanLongitude
|
|
+ 1.915 * sin(meanAnomaly.radians)
|
|
+ 0.020 * sin((2.0 * meanAnomaly).radians)
|
|
let obliquity = 23.439 - 0.000_000_4 * n
|
|
|
|
let rightAscension = atan2(
|
|
cos(obliquity.radians) * sin(lambda.radians),
|
|
cos(lambda.radians)
|
|
).degrees
|
|
let declination = asin(sin(obliquity.radians) * sin(lambda.radians)).degrees
|
|
|
|
let utcHours = date.utcHours
|
|
let lst = normalizeDegrees(100.46 + 0.985_647 * n + coordinate.longitude + 15.0 * utcHours + localOffsetHours)
|
|
let hourAngle = normalizeDegrees(lst - rightAscension)
|
|
let signedHourAngle = hourAngle > 180.0 ? hourAngle - 360.0 : hourAngle
|
|
|
|
let latitude = coordinate.latitude.radians
|
|
let declinationRad = declination.radians
|
|
let hourAngleRad = signedHourAngle.radians
|
|
|
|
let elevation = asin(
|
|
sin(latitude) * sin(declinationRad)
|
|
+ cos(latitude) * cos(declinationRad) * cos(hourAngleRad)
|
|
).degrees
|
|
|
|
let azimuth = normalizeDegrees(
|
|
atan2(
|
|
-sin(hourAngleRad),
|
|
tan(declinationRad) * cos(latitude) - sin(latitude) * cos(hourAngleRad)
|
|
).degrees
|
|
)
|
|
|
|
return SunPosition(azimuth: azimuth, elevation: elevation)
|
|
}
|
|
|
|
static func sunPathSamples(for date: Date, coordinate: CLLocationCoordinate2D) -> [Date: SunPosition] {
|
|
let calendar = Calendar.current
|
|
let sampleHours = [8, 10, 12, 14, 16]
|
|
var output: [Date: SunPosition] = [:]
|
|
|
|
for hour in sampleHours {
|
|
if let sampleDate = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: date) {
|
|
output[sampleDate] = calculateSunPosition(date: sampleDate, coordinate: coordinate)
|
|
}
|
|
}
|
|
return output
|
|
}
|
|
|
|
private static func normalizeDegrees(_ value: Double) -> Double {
|
|
let reduced = value.truncatingRemainder(dividingBy: 360.0)
|
|
return reduced >= 0 ? reduced : reduced + 360.0
|
|
}
|
|
}
|
|
|
|
private extension Date {
|
|
var utcHours: Double {
|
|
let calendar = Calendar(identifier: .gregorian)
|
|
let comps = calendar.dateComponents(in: TimeZone(secondsFromGMT: 0)!, from: self)
|
|
let hours = Double(comps.hour ?? 0)
|
|
let minutes = Double(comps.minute ?? 0)
|
|
let seconds = Double(comps.second ?? 0)
|
|
return hours + minutes / 60.0 + seconds / 3600.0
|
|
}
|
|
|
|
var julianDay: Double {
|
|
let interval = timeIntervalSince1970
|
|
return (interval / 86_400.0) + 2_440_587.5
|
|
}
|
|
}
|
|
|
|
private extension Double {
|
|
var radians: Double { self * .pi / 180.0 }
|
|
var degrees: Double { self * 180.0 / .pi }
|
|
}
|