summaryrefslogtreecommitdiffstats
path: root/Solsnu.Widget/Utilities/SunTimes.swift
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-03-24 12:29:47 +0100
committerivar <i@oiee.no>2026-03-24 12:29:47 +0100
commitc0f84ad32693afed9d53a5962303e91172e95a3d (patch)
tree4b6f4d3546cf66d686b3d85bbd68df5c62a5a7c5 /Solsnu.Widget/Utilities/SunTimes.swift
parentdef2bdee1007f05d0fb6f1a69b2817d2e03b2c54 (diff)
downloadsolverv-c0f84ad32693afed9d53a5962303e91172e95a3d.tar.xz
solverv-c0f84ad32693afed9d53a5962303e91172e95a3d.zip
feat: add location fetching and sun time calculation to widget provider
- Fetch cached user location from AppGroupManager (via App Group storage) - Check if location is fresh (< 24 hours old) - Calculate sunrise/sunset times using SunTimes utility if location is fresh - Pass sun times to SolvervDef for widget display - Create widget-local copies of AppGroupManager and SunTimes utilities - Widget maintains midnight refresh policy Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'Solsnu.Widget/Utilities/SunTimes.swift')
-rw-r--r--Solsnu.Widget/Utilities/SunTimes.swift148
1 files changed, 148 insertions, 0 deletions
diff --git a/Solsnu.Widget/Utilities/SunTimes.swift b/Solsnu.Widget/Utilities/SunTimes.swift
new file mode 100644
index 0000000..f8906ce
--- /dev/null
+++ b/Solsnu.Widget/Utilities/SunTimes.swift
@@ -0,0 +1,148 @@
+import Foundation
+
+class SunTimes {
+ let latitude: Double
+ let longitude: Double
+ let date: Date
+
+ init(latitude: Double, longitude: Double, date: Date) {
+ self.latitude = latitude
+ self.longitude = longitude
+ self.date = date
+ }
+
+ /// Calculate sunrise time for the location and date
+ func sunrise() -> Date? {
+ guard let result = calculateSunTimes() else { return nil }
+ return result.sunrise
+ }
+
+ /// Calculate sunset time for the location and date
+ func sunset() -> Date? {
+ guard let result = calculateSunTimes() else { return nil }
+ return result.sunset
+ }
+
+ // NOAA solar position algorithm
+ // Reference: https://www.esrl.noaa.gov/gmd/grad/solcalc/
+ private func calculateSunTimes() -> (sunrise: Date, sunset: Date)? {
+ let calendar = Calendar(identifier: .gregorian)
+ let components = calendar.dateComponents([.year, .month, .day], from: date)
+ guard let year = components.year else {
+ return nil
+ }
+
+ // Step 1: Calculate day of year
+ let jan1 = calendar.date(from: DateComponents(year: year, month: 1, day: 1))!
+ let dayOfYear = calendar.dateComponents([.day], from: jan1, to: date).day! + 1
+
+ // Step 2: Fractional year in radians
+ let daysInYear = Double(isLeapYear(year) ? 366 : 365)
+ let gamma = 2.0 * Double.pi * Double(dayOfYear - 1) / daysInYear
+
+ // Step 3: Solar declination (radians)
+ let decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma)
+ - 0.006758 * cos(2.0 * gamma) + 0.000907 * sin(2.0 * gamma)
+ - 0.002697 * cos(3.0 * gamma) + 0.00148 * sin(3.0 * gamma)
+
+ // Step 4: Equation of time (minutes)
+ let eot = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma)
+ - 0.014615 * cos(2.0 * gamma) - 0.040849 * sin(2.0 * gamma))
+
+ // Step 5: Hour angle at sunrise/sunset
+ let latRad = latitude * Double.pi / 180.0
+ let cosH = -tan(latRad) * tan(decl)
+
+ guard cosH >= -1.0 && cosH <= 1.0 else {
+ // Sun is always up or always down at this latitude/date
+ return nil
+ }
+
+ let h = acos(cosH) * 180.0 / Double.pi
+
+ // Step 6: Solar noon in UTC (decimal hours)
+ let solarNoonUTC = 12.0 - (longitude / 15.0) - (eot / 60.0)
+
+ // Step 7: Sunrise and sunset in UTC (decimal hours)
+ let sunriseUTC = solarNoonUTC - (h / 15.0)
+ let sunsetUTC = solarNoonUTC + (h / 15.0)
+
+ // Step 8: Convert to local time (add timezone offset)
+ let tzOffset = Double(TimeZone.current.secondsFromGMT(for: date)) / 3600.0
+ let sunriseLocal = sunriseUTC + tzOffset
+ let sunsetLocal = sunsetUTC + tzOffset
+
+ // Step 9: Create date components, handling day boundary crossing
+ let calendar2 = Calendar.current
+ var baseComponents = calendar2.dateComponents([.year, .month, .day], from: date)
+
+ // Sunrise
+ var sunriseComp = baseComponents
+ let srHourDouble = sunriseLocal
+ let srHour = Int(srHourDouble)
+ let srMinuteDouble = (srHourDouble - Double(srHour)) * 60.0
+ let srMinute = Int(srMinuteDouble)
+
+ if srHour < 0 {
+ if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
+ let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
+ sunriseComp = prevComponents
+ sunriseComp.hour = 24 + srHour
+ } else {
+ sunriseComp.hour = 0
+ }
+ } else if srHour >= 24 {
+ if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
+ let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
+ sunriseComp = nextComponents
+ sunriseComp.hour = srHour - 24
+ } else {
+ sunriseComp.hour = 23
+ }
+ } else {
+ sunriseComp.hour = srHour
+ }
+ sunriseComp.minute = max(0, min(59, srMinute))
+ sunriseComp.second = 0
+
+ // Sunset
+ var sunsetComp = baseComponents
+ let ssHourDouble = sunsetLocal
+ let ssHour = Int(ssHourDouble)
+ let ssMinuteDouble = (ssHourDouble - Double(ssHour)) * 60.0
+ let ssMinute = Int(ssMinuteDouble)
+
+ if ssHour < 0 {
+ if let prevDay = calendar2.date(byAdding: .day, value: -1, to: date) {
+ let prevComponents = calendar2.dateComponents([.year, .month, .day], from: prevDay)
+ sunsetComp = prevComponents
+ sunsetComp.hour = 24 + ssHour
+ } else {
+ sunsetComp.hour = 0
+ }
+ } else if ssHour >= 24 {
+ if let nextDay = calendar2.date(byAdding: .day, value: 1, to: date) {
+ let nextComponents = calendar2.dateComponents([.year, .month, .day], from: nextDay)
+ sunsetComp = nextComponents
+ sunsetComp.hour = ssHour - 24
+ } else {
+ sunsetComp.hour = 23
+ }
+ } else {
+ sunsetComp.hour = ssHour
+ }
+ sunsetComp.minute = max(0, min(59, ssMinute))
+ sunsetComp.second = 0
+
+ guard let sunriseDate = calendar2.date(from: sunriseComp),
+ let sunsetDate = calendar2.date(from: sunsetComp) else {
+ return nil
+ }
+
+ return (sunriseDate, sunsetDate)
+ }
+
+ private func isLeapYear(_ year: Int) -> Bool {
+ return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
+ }
+}