You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

218 lines
7.3 KiB

// Copyright © 2015 Abhishek Banthia
import Cocoa
import CoreLocation
public struct Solar {
/// The coordinate that is used for the calculation
public let coordinate: CLLocationCoordinate2D
6 years ago
/// The date to generate sunrise / sunset times for
6 years ago
public private(set) var date: Date
6 years ago
6 years ago
public private(set) var sunrise: Date?
public private(set) var sunset: Date?
public private(set) var civilSunrise: Date?
public private(set) var civilSunset: Date?
public private(set) var nauticalSunrise: Date?
public private(set) var nauticalSunset: Date?
public private(set) var astronomicalSunrise: Date?
public private(set) var astronomicalSunset: Date?
6 years ago
// MARK: Init
6 years ago
public init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) {
self.date = date
6 years ago
guard CLLocationCoordinate2DIsValid(coordinate) else {
return nil
}
6 years ago
self.coordinate = coordinate
6 years ago
// Fill this Solar object with relevant data
calculate()
}
6 years ago
// MARK: - Public functions
6 years ago
/// Sets all of the Solar object's sunrise / sunset variables, if possible.
/// - Note: Can return `nil` objects if sunrise / sunset does not occur on that day.
public mutating func calculate() {
sunrise = calculate(.sunrise, for: date, and: .official)
sunset = calculate(.sunset, for: date, and: .official)
civilSunrise = calculate(.sunrise, for: date, and: .civil)
civilSunset = calculate(.sunset, for: date, and: .civil)
nauticalSunrise = calculate(.sunrise, for: date, and: .nautical)
nauticalSunset = calculate(.sunset, for: date, and: .nautical)
astronomicalSunrise = calculate(.sunrise, for: date, and: .astronimical)
astronomicalSunset = calculate(.sunset, for: date, and: .astronimical)
}
6 years ago
// MARK: - Private functions
6 years ago
6 years ago
private enum SunriseSunset {
case sunrise
case sunset
}
6 years ago
/// Used for generating several of the possible sunrise / sunset times
6 years ago
private enum Zenith: Double {
case official = 90.83
case civil = 96
case nautical = 102
case astronimical = 108
}
6 years ago
6 years ago
private func calculate(_ sunriseSunset: SunriseSunset, for date: Date, and zenith: Zenith) -> Date? {
guard let utcTimezone = TimeZone(identifier: "UTC") else { return nil }
6 years ago
// Get the day of the year
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = utcTimezone
guard let dayInt = calendar.ordinality(of: .day, in: .year, for: date) else { return nil }
let day = Double(dayInt)
6 years ago
// Convert longitude to hour value and calculate an approx. time
let lngHour = coordinate.longitude / 15
6 years ago
let hourTime: Double = sunriseSunset == .sunrise ? 6 : 18
6 years ago
let time = day + ((hourTime - lngHour) / 24)
6 years ago
// Calculate the suns mean anomaly
6 years ago
let meanAnomaly = (0.9856 * time) - 3.289
6 years ago
// Calculate the sun's true longitude
6 years ago
let subexpression1 = 1.916 * sin(meanAnomaly.degreesToRadians)
let subexpression2 = 0.020 * sin(2 * meanAnomaly.degreesToRadians)
var longitude = meanAnomaly + subexpression1 + subexpression2 + 282.634
6 years ago
// Normalise L into [0, 360] range
6 years ago
longitude = normalise(longitude, withMaximum: 360)
6 years ago
// Calculate the Sun's right ascension
6 years ago
var rightAscenscion = atan(0.91764 * tan(longitude.degreesToRadians)).radiansToDegrees
6 years ago
// Normalise RA into [0, 360] range
6 years ago
rightAscenscion = normalise(rightAscenscion, withMaximum: 360)
6 years ago
// Right ascension value needs to be in the same quadrant as L...
6 years ago
let leftQuadrant = floor(longitude / 90) * 90
let rightQuadrant = floor(rightAscenscion / 90) * 90
rightAscenscion += (leftQuadrant - rightQuadrant)
6 years ago
// Convert RA into hours
6 years ago
rightAscenscion /= 15
6 years ago
// Calculate Sun's declination
6 years ago
let sinDec = 0.39782 * sin(longitude.degreesToRadians)
let cosDec = cos(asin(sinDec))
6 years ago
// Calculate the Sun's local hour angle
let cosH = (cos(zenith.rawValue.degreesToRadians) - (sinDec * sin(coordinate.latitude.degreesToRadians))) / (cosDec * cos(coordinate.latitude.degreesToRadians))
6 years ago
// No sunrise
guard cosH < 1 else {
return nil
}
6 years ago
// No sunset
guard cosH > -1 else {
return nil
}
6 years ago
// Finish calculating H and convert into hours
let tempH = sunriseSunset == .sunrise ? 360 - acos(cosH).radiansToDegrees : acos(cosH).radiansToDegrees
6 years ago
let hours = tempH / 15.0
6 years ago
// Calculate local mean time of rising
6 years ago
let localMeanRisingTime = hours + rightAscenscion - (0.06571 * time) - 6.622
6 years ago
// Adjust time back to UTC
6 years ago
var utcCompatibleTime = localMeanRisingTime - lngHour
6 years ago
// Normalise UT into [0, 24] range
6 years ago
utcCompatibleTime = normalise(utcCompatibleTime, withMaximum: 24)
6 years ago
// Calculate all of the sunrise's / sunset's date components
6 years ago
let hour = floor(utcCompatibleTime)
let minute = floor((utcCompatibleTime - hour) * 60.0)
let second = (((utcCompatibleTime - hour) * 60) - minute) * 60.0
6 years ago
6 years ago
let shouldBeYesterday = lngHour > 0 && utcCompatibleTime > 12 && sunriseSunset == .sunrise
let shouldBeTomorrow = lngHour < 0 && utcCompatibleTime < 12 && sunriseSunset == .sunset
6 years ago
let setDate: Date
if shouldBeYesterday {
setDate = Date(timeInterval: -(60 * 60 * 24), since: date)
} else if shouldBeTomorrow {
6 years ago
setDate = Date(timeInterval: 60 * 60 * 24, since: date)
} else {
setDate = date
}
6 years ago
var components = calendar.dateComponents([.day, .month, .year], from: setDate)
components.hour = Int(hour)
components.minute = Int(minute)
components.second = Int(second)
6 years ago
calendar.timeZone = utcTimezone
return calendar.date(from: components)
}
6 years ago
/// Normalises a value between 0 and `maximum`, by adding or subtracting `maximum`
6 years ago
private func normalise(_ value: Double, withMaximum maximum: Double) -> Double {
var value = value
6 years ago
if value < 0 {
value += maximum
}
6 years ago
if value > maximum {
value -= maximum
}
6 years ago
return value
}
}
public extension Solar {
/// Whether the location specified by the `latitude` and `longitude` is in daytime on `date`
/// - Complexity: O(1)
var isDaytime: Bool {
guard
let sunrise = sunrise,
let sunset = sunset
6 years ago
else {
return false
}
6 years ago
let beginningOfDay = sunrise.timeIntervalSince1970
let endOfDay = sunset.timeIntervalSince1970
6 years ago
let currentTime = date.timeIntervalSince1970
6 years ago
let isSunriseOrLater = currentTime >= beginningOfDay
let isBeforeSunset = currentTime < endOfDay
6 years ago
return isSunriseOrLater && isBeforeSunset
}
6 years ago
/// Whether the location specified by the `latitude` and `longitude` is in nighttime on `date`
/// - Complexity: O(1)
var isNighttime: Bool {
return !isDaytime
}
}
// MARK: - Helper extensions
private extension Double {
var degreesToRadians: Double {
return Double(self) * (Double.pi / 180.0)
}
6 years ago
var radiansToDegrees: Double {
return (Double(self) * 180.0) / Double.pi
}
}