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.
217 lines
7.3 KiB
217 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 |
|
|
|
/// The date to generate sunrise / sunset times for |
|
public private(set) var date: Date |
|
|
|
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? |
|
|
|
// MARK: Init |
|
|
|
public init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) { |
|
self.date = date |
|
|
|
guard CLLocationCoordinate2DIsValid(coordinate) else { |
|
return nil |
|
} |
|
|
|
self.coordinate = coordinate |
|
|
|
// Fill this Solar object with relevant data |
|
calculate() |
|
} |
|
|
|
// MARK: - Public functions |
|
|
|
/// 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) |
|
} |
|
|
|
// MARK: - Private functions |
|
|
|
private enum SunriseSunset { |
|
case sunrise |
|
case sunset |
|
} |
|
|
|
/// Used for generating several of the possible sunrise / sunset times |
|
private enum Zenith: Double { |
|
case official = 90.83 |
|
case civil = 96 |
|
case nautical = 102 |
|
case astronimical = 108 |
|
} |
|
|
|
private func calculate(_ sunriseSunset: SunriseSunset, for date: Date, and zenith: Zenith) -> Date? { |
|
guard let utcTimezone = TimeZone(identifier: "UTC") else { return nil } |
|
|
|
// 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) |
|
|
|
// Convert longitude to hour value and calculate an approx. time |
|
let lngHour = coordinate.longitude / 15 |
|
|
|
let hourTime: Double = sunriseSunset == .sunrise ? 6 : 18 |
|
let time = day + ((hourTime - lngHour) / 24) |
|
|
|
// Calculate the suns mean anomaly |
|
let meanAnomaly = (0.9856 * time) - 3.289 |
|
|
|
// Calculate the sun's true longitude |
|
let subexpression1 = 1.916 * sin(meanAnomaly.degreesToRadians) |
|
let subexpression2 = 0.020 * sin(2 * meanAnomaly.degreesToRadians) |
|
var longitude = meanAnomaly + subexpression1 + subexpression2 + 282.634 |
|
|
|
// Normalise L into [0, 360] range |
|
longitude = normalise(longitude, withMaximum: 360) |
|
|
|
// Calculate the Sun's right ascension |
|
var rightAscenscion = atan(0.91764 * tan(longitude.degreesToRadians)).radiansToDegrees |
|
|
|
// Normalise RA into [0, 360] range |
|
rightAscenscion = normalise(rightAscenscion, withMaximum: 360) |
|
|
|
// Right ascension value needs to be in the same quadrant as L... |
|
let leftQuadrant = floor(longitude / 90) * 90 |
|
let rightQuadrant = floor(rightAscenscion / 90) * 90 |
|
rightAscenscion += (leftQuadrant - rightQuadrant) |
|
|
|
// Convert RA into hours |
|
rightAscenscion /= 15 |
|
|
|
// Calculate Sun's declination |
|
let sinDec = 0.39782 * sin(longitude.degreesToRadians) |
|
let cosDec = cos(asin(sinDec)) |
|
|
|
// Calculate the Sun's local hour angle |
|
let cosH = (cos(zenith.rawValue.degreesToRadians) - (sinDec * sin(coordinate.latitude.degreesToRadians))) / (cosDec * cos(coordinate.latitude.degreesToRadians)) |
|
|
|
// No sunrise |
|
guard cosH < 1 else { |
|
return nil |
|
} |
|
|
|
// No sunset |
|
guard cosH > -1 else { |
|
return nil |
|
} |
|
|
|
// Finish calculating H and convert into hours |
|
let tempH = sunriseSunset == .sunrise ? 360 - acos(cosH).radiansToDegrees : acos(cosH).radiansToDegrees |
|
let hours = tempH / 15.0 |
|
|
|
// Calculate local mean time of rising |
|
let localMeanRisingTime = hours + rightAscenscion - (0.06571 * time) - 6.622 |
|
|
|
// Adjust time back to UTC |
|
var utcCompatibleTime = localMeanRisingTime - lngHour |
|
|
|
// Normalise UT into [0, 24] range |
|
utcCompatibleTime = normalise(utcCompatibleTime, withMaximum: 24) |
|
|
|
// Calculate all of the sunrise's / sunset's date components |
|
let hour = floor(utcCompatibleTime) |
|
let minute = floor((utcCompatibleTime - hour) * 60.0) |
|
let second = (((utcCompatibleTime - hour) * 60) - minute) * 60.0 |
|
|
|
let shouldBeYesterday = lngHour > 0 && utcCompatibleTime > 12 && sunriseSunset == .sunrise |
|
let shouldBeTomorrow = lngHour < 0 && utcCompatibleTime < 12 && sunriseSunset == .sunset |
|
|
|
let setDate: Date |
|
if shouldBeYesterday { |
|
setDate = Date(timeInterval: -(60 * 60 * 24), since: date) |
|
} else if shouldBeTomorrow { |
|
setDate = Date(timeInterval: 60 * 60 * 24, since: date) |
|
} else { |
|
setDate = date |
|
} |
|
|
|
var components = calendar.dateComponents([.day, .month, .year], from: setDate) |
|
components.hour = Int(hour) |
|
components.minute = Int(minute) |
|
components.second = Int(second) |
|
|
|
calendar.timeZone = utcTimezone |
|
return calendar.date(from: components) |
|
} |
|
|
|
/// Normalises a value between 0 and `maximum`, by adding or subtracting `maximum` |
|
private func normalise(_ value: Double, withMaximum maximum: Double) -> Double { |
|
var value = value |
|
|
|
if value < 0 { |
|
value += maximum |
|
} |
|
|
|
if value > maximum { |
|
value -= maximum |
|
} |
|
|
|
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 |
|
else { |
|
return false |
|
} |
|
|
|
let beginningOfDay = sunrise.timeIntervalSince1970 |
|
let endOfDay = sunset.timeIntervalSince1970 |
|
let currentTime = date.timeIntervalSince1970 |
|
|
|
let isSunriseOrLater = currentTime >= beginningOfDay |
|
let isBeforeSunset = currentTime < endOfDay |
|
|
|
return isSunriseOrLater && isBeforeSunset |
|
} |
|
|
|
/// 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) |
|
} |
|
|
|
var radiansToDegrees: Double { |
|
return (Double(self) * 180.0) / Double.pi |
|
} |
|
}
|
|
|