diff options
Diffstat (limited to 'Solverv')
| -rw-r--r-- | Solverv/Models/Season.swift | 49 | ||||
| -rw-r--r-- | Solverv/Models/SolsticeData.swift | 114 | ||||
| -rw-r--r-- | Solverv/Models/SolsticeEvent.swift | 38 | ||||
| -rw-r--r-- | Solverv/SolvervApp.swift | 10 | ||||
| -rw-r--r-- | Solverv/Utilities/AppGroupManager.swift | 67 | ||||
| -rw-r--r-- | Solverv/Utilities/SunTimes.swift | 148 |
6 files changed, 1 insertions, 425 deletions
diff --git a/Solverv/Models/Season.swift b/Solverv/Models/Season.swift deleted file mode 100644 index 01eaf99..0000000 --- a/Solverv/Models/Season.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -enum Season: String, Codable { - case spring - case summer - case autumn - case winter - - var displayName: String { - switch self { - case .spring: return "Spring" - case .summer: return "Summer" - case .autumn: return "Autumn" - case .winter: return "Winter" - } - } - - var description: String { - switch self { - case .spring: return "Day and night are approximately equal length" - case .summer: return "Longest day of the year" - case .autumn: return "Day and night are approximately equal length" - case .winter: return "Shortest day of the year" - } - } - - var colorLight: Color { - switch self { - case .spring: return Color(red: 0.298, green: 0.686, blue: 0.314) // #4CAF50 - case .summer: return Color(red: 1.0, green: 0.761, blue: 0.039) // #FFC107 - case .autumn: return Color(red: 1.0, green: 0.596, blue: 0.0) // #FF9800 - case .winter: return Color(red: 0.129, green: 0.588, blue: 0.953) // #2196F3 - } - } - - var assetName: String { - return "Season\(displayName)" - } - - static func fromDate(_ date: Date) -> Season { - let month = Calendar.current.component(.month, from: date) - switch month { - case 3, 4, 5: return .spring - case 6, 7, 8: return .summer - case 9, 10, 11: return .autumn - default: return .winter - } - } -} diff --git a/Solverv/Models/SolsticeData.swift b/Solverv/Models/SolsticeData.swift deleted file mode 100644 index 5a36da7..0000000 --- a/Solverv/Models/SolsticeData.swift +++ /dev/null @@ -1,114 +0,0 @@ -import Foundation - -class SolsticeData { - static let shared = SolsticeData() - - private let events: [SolsticeEvent] - - private init() { - // Hardcoded solstice/equinox events for 2025-2030 (all in UTC) - var allEvents: [SolsticeEvent] = [] - - // 2025 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 3, day: 20, hour: 9, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 6, day: 20, hour: 14, minute: 42), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 9, day: 22, hour: 18, minute: 20), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 12, day: 21, hour: 15, minute: 3), season: .winter)) - - // 2026 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 3, day: 20, hour: 14, minute: 46), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 6, day: 21, hour: 8, minute: 25), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 9, day: 23, hour: 0, minute: 6), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 12, day: 21, hour: 20, minute: 50), season: .winter)) - - // 2027 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 3, day: 20, hour: 20, minute: 25), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 6, day: 21, hour: 14, minute: 11), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 9, day: 23, hour: 6, minute: 2), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 12, day: 22, hour: 2, minute: 43), season: .winter)) - - // 2028 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 3, day: 20, hour: 2, minute: 17), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 6, day: 20, hour: 20, minute: 2), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 9, day: 22, hour: 11, minute: 45), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 12, day: 21, hour: 8, minute: 20), season: .winter)) - - // 2029 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 3, day: 20, hour: 8, minute: 1), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 6, day: 21, hour: 1, minute: 48), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 9, day: 22, hour: 17, minute: 37), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 12, day: 21, hour: 14, minute: 14), season: .winter)) - - // 2030 - allEvents.append(SolsticeEvent(name: "Spring Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 3, day: 20, hour: 13, minute: 51), season: .spring)) - allEvents.append(SolsticeEvent(name: "Summer Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 6, day: 21, hour: 7, minute: 31), season: .summer)) - allEvents.append(SolsticeEvent(name: "Autumn Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 9, day: 22, hour: 23, minute: 27), season: .autumn)) - allEvents.append(SolsticeEvent(name: "Winter Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 12, day: 21, hour: 20, minute: 9), season: .winter)) - - // Sort events by date - self.events = allEvents.sorted { $0.date < $1.date } - } - - /// Helper function to create UTC dates (static to be usable during init) - private static func dateFromUTC(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date { - var components = DateComponents() - components.year = year - components.month = month - components.day = day - components.hour = hour - components.minute = minute - components.second = 0 - components.timeZone = TimeZone(abbreviation: "UTC") - - return Calendar(identifier: .gregorian).date(from: components) ?? Date() - } - - /// Returns the next upcoming solstice/equinox event - func nextEvent() -> SolsticeEvent? { - let now = Date() - return events.first { $0.date > now } - } - - /// Returns the next N upcoming events - func upcomingEvents(count: Int) -> [SolsticeEvent] { - let now = Date() - let futureEvents = events.filter { $0.date > now } - return Array(futureEvents.prefix(count)) - } - - /// Returns the progress to the next event as (elapsed days, total days) - func progressToNextEvent() -> (elapsed: Int, total: Int)? { - guard let nextEvent = nextEvent() else { - return nil - } - - let now = Date() - let today = Calendar.current.startOfDay(for: now) - let eventDay = Calendar.current.startOfDay(for: nextEvent.date) - - // Find the previous event to calculate total days - guard let previousEventIndex = events.firstIndex(where: { $0.date > now }) else { - return nil - } - - let previousEvent: Date - if previousEventIndex > 0 { - previousEvent = events[previousEventIndex - 1].date - } else { - // If this is the first event, we need to handle this case - // Use the event itself minus some arbitrary period (not applicable for first event) - return nil - } - - let previousEventDay = Calendar.current.startOfDay(for: previousEvent) - - // Calculate elapsed and total days - let elapsedComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: today) - let totalComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: eventDay) - - let elapsed = max(0, elapsedComponents.day ?? 0) - let total = max(1, totalComponents.day ?? 1) - - return (elapsed: elapsed, total: total) - } -} diff --git a/Solverv/Models/SolsticeEvent.swift b/Solverv/Models/SolsticeEvent.swift deleted file mode 100644 index d8c4a7b..0000000 --- a/Solverv/Models/SolsticeEvent.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -struct SolsticeEvent: Identifiable, Codable { - let id: UUID - let name: String - let date: Date // UTC - let season: Season - - init(name: String, date: Date, season: Season) { - self.id = UUID() - self.name = name - self.date = date - self.season = season - } - - /// Convert UTC date to user's local timezone - func localDateTime() -> Date { - let utcCalendar = Calendar(identifier: .gregorian) - let utcComponents = utcCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) - let timeZone = TimeZone.current - let offset = timeZone.secondsFromGMT(for: date) - - var localCalendar = Calendar.current - localCalendar.timeZone = timeZone - var localComponents = utcComponents - localComponents.second = (localComponents.second ?? 0) + offset - - return localCalendar.date(from: localComponents) ?? date - } - - /// Days until this event from today - func daysUntil() -> Int { - let today = Calendar.current.startOfDay(for: Date()) - let eventDay = Calendar.current.startOfDay(for: date) - let components = Calendar.current.dateComponents([.day], from: today, to: eventDay) - return max(0, components.day ?? 0) - } -} diff --git a/Solverv/SolvervApp.swift b/Solverv/SolvervApp.swift index a2beb1d..955a946 100644 --- a/Solverv/SolvervApp.swift +++ b/Solverv/SolvervApp.swift @@ -52,15 +52,6 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject { manager.requestWhenInUseAuthorization() } else if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways { fetchLocation() - } else { - // Use default location (Greenwich) - let defaultLocation = AppGroupManager.UserLocation( - latitude: 0.0, - longitude: 0.0, - timestamp: ISO8601DateFormatter().string(from: Date()), - isDefaultLocation: true - ) - AppGroupManager.shared.saveLocation(defaultLocation) } } @@ -83,6 +74,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject { timestamp: ISO8601DateFormatter().string(from: Date()), isDefaultLocation: false ) + AppGroupManager.shared.saveLocation(userLocation) // Calculate and cache sunrise/sunset diff --git a/Solverv/Utilities/AppGroupManager.swift b/Solverv/Utilities/AppGroupManager.swift deleted file mode 100644 index 95ff2de..0000000 --- a/Solverv/Utilities/AppGroupManager.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -class AppGroupManager { - static let shared = AppGroupManager() - static let appGroupID = "group.com.ivarlovlie.solverv" - - private lazy var userDefaults: UserDefaults? = { - UserDefaults(suiteName: Self.appGroupID) - }() - - // MARK: - Location Storage - - struct UserLocation: Codable { - let latitude: Double - let longitude: Double - let timestamp: String // ISO 8601 - let isDefaultLocation: Bool - } - - func saveLocation(_ location: UserLocation) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(location) { - ud.set(encoded, forKey: "userLocation") - } - } - - func getLocation() -> UserLocation? { - guard let ud = userDefaults, - let data = ud.data(forKey: "userLocation"), - let location = try? JSONDecoder().decode(UserLocation.self, from: data) else { - return nil - } - return location - } - - // MARK: - Sunrise/Sunset Storage - - struct SunTimes: Codable { - let date: String // ISO 8601 date only (YYYY-MM-DD) - let sunrise: String // ISO 8601 datetime - let sunset: String // ISO 8601 datetime - let timestamp: String // ISO 8601 when calculated - } - - func saveSunTimes(_ sunTimes: SunTimes) { - guard let ud = userDefaults else { return } - if let encoded = try? JSONEncoder().encode(sunTimes) { - ud.set(encoded, forKey: "sunTimes") - } - } - - func getSunTimes() -> SunTimes? { - guard let ud = userDefaults, - let data = ud.data(forKey: "sunTimes"), - let sunTimes = try? JSONDecoder().decode(SunTimes.self, from: data) else { - return nil - } - return sunTimes - } - - // MARK: - Helpers - - func clearAllData() { - userDefaults?.removeObject(forKey: "userLocation") - userDefaults?.removeObject(forKey: "sunTimes") - } -} diff --git a/Solverv/Utilities/SunTimes.swift b/Solverv/Utilities/SunTimes.swift deleted file mode 100644 index 3b8f049..0000000 --- a/Solverv/Utilities/SunTimes.swift +++ /dev/null @@ -1,148 +0,0 @@ -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 - let 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) - } -} |
