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 } }