feat: Ipad app features and Dream Weaver for Velocity WebOS
This commit is contained in:
134
iOS/velocity-ipad/velocity/Core/Math/SunMath.swift
Normal file
134
iOS/velocity-ipad/velocity/Core/Math/SunMath.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
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 {
|
||||
|
||||
// MARK: - Single Position
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Hourly Arc (used by legacy code & DashedSunLine)
|
||||
|
||||
/// 5-sample dictionary kept for backward compat with the Dollhouse slider.
|
||||
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
|
||||
}
|
||||
|
||||
/// Dense arc for the AR overlay — one sample per hour from 4 AM to 8 PM.
|
||||
/// Filters out below-horizon positions (elevation < -5°).
|
||||
static func sunPathArc(for date: Date, coordinate: CLLocationCoordinate2D) -> [(date: Date, position: SunPosition)] {
|
||||
let calendar = Calendar.current
|
||||
var result: [(Date, SunPosition)] = []
|
||||
for hour in 4...20 {
|
||||
guard let sampleDate = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: date) else { continue }
|
||||
let pos = calculateSunPosition(date: sampleDate, coordinate: coordinate)
|
||||
// include a small below-horizon buffer so arc starts/ends smoothly
|
||||
if pos.elevation > -5 {
|
||||
result.append((sampleDate, pos))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Approximate sunrise and sunset by scanning for elevation sign changes.
|
||||
static func sunRiseSet(for date: Date, coordinate: CLLocationCoordinate2D) -> (rise: Date?, set: Date?) {
|
||||
let calendar = Calendar.current
|
||||
var rise: Date? = nil
|
||||
var set: Date? = nil
|
||||
var prevElevation: Double? = nil
|
||||
var prevDate: Date? = nil
|
||||
|
||||
for minuteOffset in stride(from: 0, through: 24 * 60, by: 10) {
|
||||
guard let sampleDate = calendar.date(byAdding: .minute, value: minuteOffset, to: calendar.startOfDay(for: date)) else { continue }
|
||||
let pos = calculateSunPosition(date: sampleDate, coordinate: coordinate)
|
||||
if let prev = prevElevation, let prevD = prevDate {
|
||||
if prev < 0 && pos.elevation >= 0 { rise = prevD }
|
||||
if prev >= 0 && pos.elevation < 0 { set = prevD }
|
||||
}
|
||||
prevElevation = pos.elevation
|
||||
prevDate = sampleDate
|
||||
}
|
||||
return (rise, set)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
static func normalizeDegrees(_ value: Double) -> Double {
|
||||
let reduced = value.truncatingRemainder(dividingBy: 360.0)
|
||||
return reduced >= 0 ? reduced : reduced + 360.0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Date helpers
|
||||
|
||||
private extension Date {
|
||||
var utcHours: Double {
|
||||
let calendar = Calendar(identifier: .gregorian)
|
||||
let comps = calendar.dateComponents(in: TimeZone(secondsFromGMT: 0)!, from: self)
|
||||
return Double(comps.hour ?? 0) + Double(comps.minute ?? 0) / 60.0 + Double(comps.second ?? 0) / 3600.0
|
||||
}
|
||||
|
||||
var julianDay: Double {
|
||||
timeIntervalSince1970 / 86_400.0 + 2_440_587.5
|
||||
}
|
||||
}
|
||||
|
||||
private extension Double {
|
||||
var radians: Double { self * .pi / 180.0 }
|
||||
var degrees: Double { self * 180.0 / .pi }
|
||||
}
|
||||
Reference in New Issue
Block a user