10 |
11 | **SunKit** is a Swift package that uses math and trigonometry to compute several pieces of information about the Sun.
12 |
13 | SunKit was first developed as part of a bigger project: [Sunlitt](https://github.com/Sunlitt/Sunlitt-AppStore). Even though Sunlitt is not meant to be open-sourced, we decided to wrap the fundamental logic of the app and build a library out of it, free for anyone to use, embrace and extend.
14 |
15 | [](https://apps.apple.com/app/sunlitt/id1628751457)
16 |
17 | ## Attribution
18 | SunKit is licensed under the Apache License 2.0. For attribution, we request that you include an unmodified vector of SunKit's logo, available [here](Resources/sunkit.svg), along with our organization name "Sunlitt", both at legible sizes, and that they link back to our [website](https://sunlitt.app).
19 |
20 | If you are developing an app for Apple platforms, we additionally request that you include SunKit's license and copyright in the "Acknowledgments" section of your app's Settings.bundle file. We have included a Settings.bundle example [here](Resources/Settings.bundle) for you to download and import as is in your Xcode project.
21 |
22 | Attribution is essential to make sure that our hard work is properly recognized and we thank you for complying with our requests.
23 |
24 |
25 | ## Usage
26 | **CoreLocation framework is required for SunKit to work**. SunKit only needs a location and the relative time zone. Everything is computed locally, no internet connection is needed.
27 |
28 | ### Creating a Sun
29 |
30 | ```swift
31 |
32 | // Creating a CLLocation object with the coordinates you are interested in
33 | let naplesLocation: CLLocation = .init(latitude: 40.84014, longitude: 14.25226)
34 |
35 | // Timezone for the location of interest. It's highly recommended to initialize it via identifier
36 | let timeZoneNaples: Timezone = .init(identifier: "Europe/Rome") ?? .current
37 |
38 | // Creating the Sun instance which will store all the information you need about sun events and his position
39 | var mySun: Sun = .init(location: naplesLocation, timeZone: timeZoneNaples)
40 |
41 | ```
42 |
43 | ### Retrieve information
44 |
45 | ```swift
46 | // Creating a Date instance
47 | let myDate: Date = Date() // Your current date
48 |
49 | // Setting inside mySun object the date of interest
50 | mySun.setDate(myDate)
51 |
52 | // All the following informations are related to the given location for the date that has just been set
53 |
54 | // Azimuth of the Sun
55 | mySun.azimuth.degrees
56 |
57 | // Altitude of the Sun
58 | mySun.altitude.degrees
59 |
60 | // Sunrise Date
61 | mySun.sunrise
62 |
63 | // Sunset Date
64 | mySun.sunset
65 |
66 | // Evening Golden Hour Start Date
67 | mySun.eveningGoldenHourStart
68 |
69 | // Evening Golden Hour End Date
70 | mySun.eveningGoldenHourEnd
71 |
72 | // To know all the information you can retrieve go to the **Features** section.
73 |
74 | ```
75 |
76 |
77 | ### Working with Timezones and Dates
78 | To properly show the Sun Date Events use the following DateFormatter.
79 |
80 | ```swift
81 |
82 | //Creting a DateFormatter
83 | let dateFormatter = DateFormatter()
84 |
85 | //Properly setting his attributes
86 | dateFormatter.locale = .current
87 | dateFormatter.timeZone = timeZoneNaples // It shall be the same as the one used to initilize mySun
88 | dateFormatter.timeStyle = .full
89 | dateFormatter.dateStyle = .full
90 |
91 | //Printing Sun Date Events with the correct Timezone
92 |
93 | print("Sunrise: \(dateFormatter.string(from: mySun.sunrise))")
94 |
95 | ```
96 |
97 | ## Features
98 | * Sun Azimuth
99 | * Sun Altitude
100 | * Civil Dusk Time
101 | * Civil Dawn Time
102 | * Sunrise Time
103 | * Solar Noon Time
104 | * Solar Midnight Time
105 | * Morning Golden Hour Time
106 | * Evening Golden Hour Time
107 | * Sunset Time
108 | * Astronomical Dusk
109 | * Astronomical Dawn
110 | * Nautical Dusk
111 | * Nautical Dawn
112 | * Morning Blue Hour Time
113 | * Evening Blue Hour Time
114 | * Sun Azimuth at Sunrise
115 | * Sun Azimuth at Sunset
116 | * Sun Azimuth at Solar Noon
117 | * Total Daylight Duration
118 | * Total Night Duration
119 | * March Equinox
120 | * June Solstice
121 | * September Equinox
122 | * December Solstice
123 |
124 |
125 | ## References
126 |
127 | * NOAA Global Monitoring Division. General Solar Position Calculations: [Link](https://gml.noaa.gov/grad/solcalc/solareqns.PDF).
128 | * PV Education: [Link](https://www.pveducation.org).
129 | * Celestial Calculations: A Gentle Introduction to Computational Astronomy: [Link](https://www.amazon.it/Celestial-Calculations-Introduction-Computational-Astronomy/dp/0262536633/ref=sr_1_1?__mk_it_IT=ÅMÅŽÕÑ&crid=1U99GMGDZ2CUF&keywords=celestial+calculations&qid=1674408445&sprefix=celestial+calculation%2Caps%2C109&sr=8-1).
130 |
131 | ## MoonKit 🌙
132 | Take a look at SunKit's spiritual brother, [MoonKit](https://github.com/davideilmito/MoonKit).
133 |
134 | ## Special thanks
135 | * [Davide Biancardi](https://github.com/davideilmito): Creator of SunKit.
136 |
--------------------------------------------------------------------------------
/Tests/SunKitTests/UT_EquatorialCoordinates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UT_EquatorialCoordinates.swift
3 | //
4 | //
5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | import XCTest
20 | @testable import SunKit
21 |
22 | final class UT_EquatorialCoordinates: XCTestCase {
23 |
24 |
25 | /// Test of EquatorialCoordinates init
26 | func testOfInitEquatorialCoordinates() throws {
27 |
28 | //Test1: Consider a star whose right ascension is 3h24m06s and declination = −0°30'30''. Suppose the LST for an observer is 18h.Calculate the corresponding hour angle.
29 |
30 | //Step1:
31 | let declinationUnderTest: Angle = .init(degrees: DMS.init(degrees:0 , minutes: 30, seconds: 30,isANegativeZero: true).dMS2Decimal())
32 | let rightAscensionUnderTest: Angle = .init(degrees: HMS.init(hours: 3, minutes: 24, seconds: 06).hMS2Decimal())
33 | var equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: declinationUnderTest, rightAscension: rightAscensionUnderTest)
34 | //Step2: Converting hour Angle from angle to decimal
35 | let hourAngleDecimal = equatorialCoordinatesUnderTest.setHourAngleFrom(lstDecimal: 18)!.degrees / 15
36 |
37 | //Step3:
38 | XCTAssertTrue(abs(hourAngleDecimal - 14.598333) < 0.1)
39 |
40 | }
41 |
42 |
43 | /// Test of equatorial2Horizon
44 | func testOfequatorial2Horizon() throws {
45 |
46 |
47 | //Test1: Convert equatorial coordinates with declination = 17.248880 and right ascension = 4.257714. Expected horizon coordinates are altitude = 68°52 and Azimuth = 192°11′. For an observer at 38° N and LST is 4.562547h
48 |
49 | //Step1:
50 | var equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: .init(degrees: 17.248880), rightAscension: .init(degrees: 4.257714))
51 | var latitudeUnderTest: Angle = .init(degrees: 38)
52 | let lstUnderTest = 4.562547
53 |
54 | //Step2: Saving expected values in output for both azimuth and altitude
55 | var expectedAltitude = DMS.init(degrees: 68, minutes: 52, seconds: 0).dMS2Decimal()
56 | var expectedAzimuth = DMS.init(degrees: 192, minutes: 11, seconds: 0).dMS2Decimal()
57 |
58 | var azimuth = equatorialCoordinatesUnderTest.equatorial2Horizon(lstDecimal: lstUnderTest, latitude: latitudeUnderTest)!.azimuth.degrees
59 | var altitude = equatorialCoordinatesUnderTest.equatorial2Horizon(lstDecimal: lstUnderTest, latitude: latitudeUnderTest)!.altitude.degrees
60 |
61 | //Step3: Check if output of the function under test is close to expected output for both azimuth and altitude
62 | XCTAssertTrue(abs(azimuth - expectedAzimuth) < 0.1)
63 | XCTAssertTrue(abs(altitude - expectedAltitude) < 0.1)
64 |
65 |
66 | //Test2: Suppose a star is located at δ = −0°30′30′′, H = 16h29m45s. For an observer at 25° N latitude. Expected output shall be Azimuth = 80°31′31′′ ,and −20°34′40′′.
67 |
68 | //Step4:
69 | let hourAngleDecimal = HMS.init(hours: 16, minutes: 29, seconds: 45).hMS2Decimal()
70 | let hourAngle: Angle = .init(degrees: hourAngleDecimal * 15)
71 | latitudeUnderTest = .degrees(25)
72 | let declinationUnderTest: Angle = .init(degrees: DMS.init(degrees: 0, minutes: 30, seconds: 30,isANegativeZero: true).dMS2Decimal())
73 |
74 | //Step5: Creating a new instance of EquatorialCoordinates initialized with declinaiton and hour angle under test
75 | equatorialCoordinatesUnderTest = .init(declination: declinationUnderTest, hourAngle: hourAngle)
76 |
77 | //Step6: Saving expected values in output for both azimuth and altitude
78 | expectedAltitude = DMS.init(degrees: -20, minutes: 34, seconds: 40).dMS2Decimal()
79 | expectedAzimuth = DMS.init(degrees: 80, minutes: 31, seconds: 31).dMS2Decimal()
80 |
81 | azimuth = equatorialCoordinatesUnderTest.equatorial2Horizon(latitude: latitudeUnderTest)!.azimuth.degrees
82 | altitude = equatorialCoordinatesUnderTest.equatorial2Horizon(latitude: latitudeUnderTest)!.altitude.degrees
83 |
84 | //Step7: Check if output of the function under test is close to expected output for both azimuth and altitude
85 | XCTAssertTrue(abs(azimuth - expectedAzimuth) < 0.1)
86 | XCTAssertTrue(abs(altitude - expectedAltitude) < 0.1)
87 | }
88 |
89 | /// Test of equatorial2Ecliptic
90 | func testOfequatorial2Ecliptic() throws {
91 |
92 | //Test1: Given Jupiter’s equatorial coordinates of right ascension 12h18m47.5s, declination −0°43′35.5'', and the standard epoch J2000, compute Jupiter’s ecliptic coordinates.Expected output shall be eclipitc latitude = 1°12′00.0′′ and ecliptic longitude = 184°36′00.0′′
93 |
94 | //Step1:
95 | let rightAscensionUnderTest = HMS.init(hours: 12, minutes: 18, seconds: 47.5).hMS2Decimal()
96 | let declinationUnderTest = DMS.init(degrees: 0, minutes: 43, seconds: 35.5,isANegativeZero: true).dMS2Decimal()
97 | let equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: .degrees(declinationUnderTest), rightAscension: .degrees(rightAscensionUnderTest))
98 |
99 | let eclipticCoordinates = equatorialCoordinatesUnderTest.equatorial2Ecliptic() ?? .init(eclipticLatitude: .zero, eclipticLongitude: .zero)
100 |
101 | //Step2: Saving expected values in output for both latitude and longitude
102 | let expectedLatitude = DMS.init(degrees: 1, minutes: 12, seconds: 0).dMS2Decimal()
103 | let expectedLongitude = DMS.init(degrees: 184, minutes: 36, seconds: 0).dMS2Decimal()
104 |
105 | //Step3: Check if output of the function under test is close to expected output for both latitude and longitude
106 | XCTAssertTrue(abs(expectedLatitude - eclipticCoordinates.eclipticLatitude.degrees) < 0.1)
107 | XCTAssertTrue(abs(expectedLongitude - eclipticCoordinates.eclipticLongitude.degrees) < 0.1)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/SunKit/EquatorialCoordinates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EquatorialCoordinates.swift
3 | //
4 | //
5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | import Foundation
20 |
21 | public struct EquatorialCoordinates: Equatable, Hashable, Codable, Sendable {
22 |
23 | public private(set) var rightAscension: Angle? // rightAscension.degrees refers to h format
24 | public private(set) var declination: Angle //delta
25 |
26 | private(set) var hourAngle: Angle?
27 |
28 | init(declination: Angle,rightAscension: Angle, hourAngle: Angle){
29 | self.declination = declination
30 | self.rightAscension = rightAscension
31 | self.hourAngle = hourAngle
32 | }
33 |
34 | init(declination: Angle,rightAscension: Angle){
35 | self.declination = declination
36 | self.rightAscension = rightAscension
37 | self.hourAngle = nil
38 | }
39 |
40 | init(declination: Angle,hourAngle: Angle){
41 | self.declination = declination
42 | self.hourAngle = hourAngle
43 | self.rightAscension = nil
44 | }
45 |
46 | init(declination: Angle){
47 | self.declination = declination
48 | }
49 |
50 | /// To set right ascension we need LST and right ascension. If right ascension is nill we can't set it.
51 | /// - Parameter lstDecimal: Local Sideral Time in decimal
52 | /// - Returns: The value of hour angle just been set. Nil if right ascension is also nil
53 | public mutating func setHourAngleFrom(lstDecimal: Double) -> Angle?{
54 |
55 | guard let rightAscension = self.rightAscension else {return nil}
56 |
57 | var hourAngleDecimal = lstDecimal - rightAscension.degrees
58 | if hourAngleDecimal < 0 {
59 | hourAngleDecimal += 24
60 | }
61 | self.hourAngle = .init(degrees: hourAngleDecimal * 15)
62 |
63 | return self.hourAngle
64 | }
65 |
66 | /// To set right ascension we need LST and hour angle. If hour angle is nill we can't set it.
67 | /// - Parameter lstDecimal: Local Sideral Time in decimal
68 | /// - Returns: The value of right ascension just been set. Nil if hour angle is also nil
69 | public mutating func setRightAscensionFrom(lstDecimal: Double) -> Angle?{
70 |
71 | guard let hourAngle = self.hourAngle else {return nil}
72 |
73 | let hourAngleDecimal = hourAngle.degrees / 15
74 |
75 | self.rightAscension = .init(degrees: lstDecimal - hourAngleDecimal)
76 |
77 | return self.rightAscension
78 | }
79 |
80 |
81 | /// Converts Equatorial coordinates to Horizon coordinates.
82 | ///
83 | /// Since horizon coordinates depend on the position, we need also latitude parameter to create an EquatorialCoordinates instance.
84 | ///
85 | /// - Parameters:
86 | /// - lstDecimal: Local Sidereal Time in decimal format.
87 | /// - latitude: Latitude of the observer
88 | /// - Returns: Horizon coordinates for the given latitude and LST. Nil if hour angle cannot be computed due to the miss right ascnsion information
89 | public mutating func equatorial2Horizon(lstDecimal: Double,latitude: Angle) -> HorizonCoordinates?{
90 |
91 | guard let _ = setHourAngleFrom(lstDecimal: lstDecimal) else {return nil}
92 |
93 | //Step4:
94 | let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians)
95 |
96 | //Step5:
97 | let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon))
98 |
99 | //Step6:
100 | let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians)
101 |
102 | //Step7:
103 | let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians))
104 |
105 | //Step8:
106 | var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon))
107 | if sin(hourAngle!.radians) >= 0{
108 | azimuth.degrees = 360 - azimuth.degrees
109 | }
110 |
111 | return .init(altitude: altitude, azimuth: azimuth)
112 | }
113 |
114 | /// Converts Equatorial coordinates to Horizon coordinates.
115 | ///
116 | /// Since horizon coordinates depend on the position, we need also latitude parameter to create an EquatorialCoordinates instance.
117 | ///
118 | /// - Parameters:
119 | /// - latitude: Latitude of the observer
120 | /// - Returns: Horizon coordinates for the given latitude. Nil if hour angle is not defined.
121 | public func equatorial2Horizon(latitude: Angle) -> HorizonCoordinates?{
122 |
123 | guard let _ = self.hourAngle else {return nil}
124 |
125 | //Step4:
126 | let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians)
127 |
128 | //Step5:
129 | let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon))
130 |
131 | //Step6:
132 | let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians)
133 |
134 | //Step7:
135 | let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians))
136 |
137 | //Step8:
138 | var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon))
139 | if sin(hourAngle!.radians) >= 0{
140 | azimuth.degrees = 360 - azimuth.degrees
141 | }
142 |
143 | return .init(altitude: altitude, azimuth: azimuth)
144 | }
145 |
146 | public func equatorial2Ecliptic() -> EclipticCoordinates?{
147 |
148 | guard var rightAscension = rightAscension else {return nil}
149 |
150 | rightAscension.degrees = rightAscension.degrees * 15 //from h format to degrees
151 |
152 | //Step5:
153 | let tEquatorialToEcliptic: Angle = .init(radians: sin(declination.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - cos(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(rightAscension.radians))
154 |
155 | //Step6:
156 | let eclipticLatitude: Angle = .init(radians: asin(tEquatorialToEcliptic.radians))
157 |
158 | //Step7:
159 | let yEquatorialToEcliptic = sin(rightAscension.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + tan(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians)
160 |
161 | //Step8:
162 | let xEquatorialToEcliptic = cos(rightAscension.radians)
163 |
164 | //Step9:
165 | var r: Angle = .init(radians: atan(yEquatorialToEcliptic / xEquatorialToEcliptic))
166 |
167 | //Step9:
168 | switch (yEquatorialToEcliptic >= 0,xEquatorialToEcliptic >= 0){
169 |
170 | case (true, true):
171 | break
172 | case (true,false):
173 | r.degrees += 180
174 | case(false,true):
175 | r.degrees += 360
176 | case(false,false):
177 | r.degrees += 180
178 | }
179 |
180 | let eclipticLongitude: Angle = .init(degrees: r.degrees)
181 |
182 | return .init(eclipticLatitude: eclipticLatitude, eclipticLongitude: eclipticLongitude)
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Sources/SunKit/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | //
4 | //
5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | import Foundation
20 |
21 | let SECONDS_IN_ONE_DAY = 86399
22 | let TWELVE_HOUR_IN_SECONDS: Double = 43200
23 | let TWO_HOURS_IN_SECONDS: Double = 7200
24 | let SECONDS_IN_TEN_MINUTES: Double = 600
25 | let SECONDS_IN_ONE_HOUR: Double = 3600
26 |
27 |
28 | /// - Parameters:
29 | /// - a: first operand
30 | /// - n: second operand
31 | /// - Returns: a % n if a is positive. If a isn't positive, it will add to the result of the modulo operation the value of the n operand until the result is positive.Please note that this function only works when both operands are integers.
32 | public func mod(_ a: Int, _ n: Int) -> Int {
33 | let r = a % n
34 | return r >= 0 ? r : r + n
35 | }
36 |
37 |
38 | /// Same as mod function described above, but this function can accept as first operand a Double and it handle the edge case where a is included between -1 and 0.
39 | /// - Parameters:
40 | /// - a: first operand
41 | /// - n: second operand
42 | /// - Returns: a % n if a is positive. If a isn't positive, it will add to the result of the modulo operation the value of the n operand until the result is positive.
43 | public func extendedMod(_ a: Double, _ n: Int) -> Double {
44 |
45 | let remainder: Double = a.truncatingRemainder(dividingBy: 1)
46 |
47 | if (a < 0 && a > -1){
48 |
49 | return Double(n) + remainder
50 | }
51 |
52 | let x = Double(mod(Int(a),n))
53 |
54 | return x + remainder
55 | }
56 |
57 |
58 |
59 | public func clamp(lower: Double, upper: Double, number: Double) -> Double {
60 |
61 | return min(upper,max(lower,number))
62 | }
63 |
64 |
65 | /// Creates a date with the UTC timezone
66 | /// - Parameters:
67 | /// - day: day of the date you want to create
68 | /// - month: month of the date you want to create
69 | /// - year: year of the date you want to create
70 | /// - hour: hour of the date you want to create
71 | /// - minute: minute of the date you want to create
72 | /// - seconds: second of the date you want to create
73 | /// - nanosecond: nanosecond of the date you want to create
74 | /// - Returns: A date with the parameters given in input. Used in combination with function jdFromDate, that accepts dates in UTC format
75 | public func createDateUTC(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{
76 |
77 | var calendarUTC:Calendar = .init(identifier: .gregorian)
78 | calendarUTC.timeZone = .init(secondsFromGMT: 0)!
79 | var dateComponents = DateComponents()
80 | dateComponents.year = year
81 | dateComponents.month = month
82 | dateComponents.day = day
83 | dateComponents.hour = hour
84 | dateComponents.minute = minute
85 | dateComponents.second = seconds
86 | dateComponents.nanosecond = nanosecond
87 |
88 | return calendarUTC.date(from: dateComponents) ?? Date()
89 | }
90 |
91 |
92 | /// Creates a date with the timezone used in your current device
93 | /// - Parameters:
94 | /// - day: day of the date you want to create
95 | /// - month: month of the date you want to create
96 | /// - year: year of the date you want to create
97 | /// - hour: hour of the date you want to create
98 | /// - minute: minute of the date you want to create
99 | /// - seconds: second of the date you want to create
100 | /// - nanosecond: nanosecond of the date you want to create
101 | /// - Returns: A date with the parameters given in input
102 | public func createDateCurrentTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{
103 |
104 | var calendar: Calendar = .init(identifier: .gregorian)
105 | calendar.timeZone = .current
106 | var dateComponents = DateComponents()
107 | dateComponents.year = year
108 | dateComponents.month = month
109 | dateComponents.day = day
110 | dateComponents.hour = hour
111 | dateComponents.minute = minute
112 | dateComponents.second = seconds
113 | dateComponents.nanosecond = nanosecond
114 |
115 | return calendar.date(from: dateComponents) ?? Date()
116 | }
117 |
118 | /// Creates a date with a custom timezone
119 | /// - Parameters:
120 | /// - day: day of the date you want to create
121 | /// - month: month of the date you want to create
122 | /// - year: year of the date you want to create
123 | /// - hour: hour of the date you want to create
124 | /// - minute: minute of the date you want to create
125 | /// - seconds: second of the date you want to create
126 | /// - nanosecond: nanosecond of the date you want to create
127 | /// - timeZone: timezone of the date
128 | /// - Returns: A date with the parameters given in input
129 | public func createDateCustomTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0, timeZone: TimeZone) -> Date{
130 |
131 | var calendar: Calendar = .init(identifier: .gregorian)
132 | calendar.timeZone = timeZone
133 | var dateComponents = DateComponents()
134 | dateComponents.year = year
135 | dateComponents.month = month
136 | dateComponents.day = day
137 | dateComponents.hour = hour
138 | dateComponents.minute = minute
139 | dateComponents.second = seconds
140 | dateComponents.nanosecond = nanosecond
141 |
142 | return calendar.date(from: dateComponents) ?? Date()
143 | }
144 |
145 |
146 | /// Converts Julian Number in a date
147 | /// - Parameter jd: Julian number to convert in an UTC date
148 | /// - Returns: The date corresponding to the julian number in input
149 | public func dateFromJd(jd : Double) -> Date {
150 | let JD_JAN_1_1970_0000GMT = 2440587.5
151 | return Date(timeIntervalSince1970: (jd - JD_JAN_1_1970_0000GMT) * 86400)
152 | }
153 |
154 |
155 | /// Converts date in his Julian Number
156 | /// - Parameter date: UTC date to convert in julian number. TimeZone of the given date shall be equals to +0000.
157 | /// - Returns: The julian day number corresponding to date in input
158 | public func jdFromDate(date : Date) -> Double {
159 | let JD_JAN_1_1970_0000GMT = 2440587.5
160 | return JD_JAN_1_1970_0000GMT + date.timeIntervalSince1970
161 | / 86400
162 | }
163 |
164 |
165 | /// Converts UT time to Greenwich Sidereal Time
166 | /// - Parameter ut: UT time to convert in GST
167 | /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time
168 | /// - Returns: GST equivalent of the UT given in input
169 | public func uT2GST(_ ut:Date) -> HMS{
170 |
171 | var calendarUTC: Calendar = .init(identifier: .gregorian)
172 | calendarUTC.timeZone = TimeZone(identifier: "GMT")!
173 |
174 | //Step1:
175 | let jd = jdFromDate(date: calendarUTC.startOfDay(for: ut))
176 |
177 | //Step2:
178 | let year = calendarUTC.component(.year, from: ut)
179 | let firstDayOfTheYear = calendarUTC.date(from: DateComponents(year: year , month: 1, day: 1)) ?? Date()
180 | let jdZero = jdFromDate(date: firstDayOfTheYear)
181 |
182 | //Step3:
183 | let days = jd - jdZero
184 |
185 | //Step4:
186 | let T = (jdZero - 2415020.0) / 36525.0
187 |
188 | //Step5:
189 | let R = 6.6460656 + 2400.051262 * T + 0.00002581*(T*T)
190 |
191 | //Step6:
192 | let B :Double = 24 - R + Double(24 * (year - 1900))
193 |
194 | //Step7:
195 | let TZero = 0.0657098 * days - B
196 |
197 | //Step8:
198 | let utDecimal = HMS.init(from: ut).hMS2Decimal()
199 |
200 | //Step9:
201 | var gstDecimal = TZero + 1.002738 * utDecimal
202 |
203 | if gstDecimal < 0 {
204 |
205 | gstDecimal += 24
206 |
207 | }else if gstDecimal >= 24 {
208 |
209 | gstDecimal -= 24
210 | }
211 |
212 | let gstHMS = HMS.init(decimal: gstDecimal)
213 |
214 | return gstHMS
215 | }
216 |
217 | /// Converts GST to Local Sidereal Time
218 | /// - Parameters:
219 | /// - gst: GST time to convert in LST
220 | /// - longitude: longitude of the observer
221 | /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time
222 | /// - Returns: LST equivalent for the GST given in input
223 | public func gST2LST(_ gst: HMS, longitude: Angle) -> HMS{
224 |
225 | //Step1:
226 | let gstDecimal = gst.hMS2Decimal()
227 |
228 | //Step2:
229 | let adjustment: Double = longitude.degrees / 15.0
230 |
231 | //Step3:
232 | var lstDecimal = gstDecimal + adjustment
233 |
234 | if lstDecimal < 0 {
235 |
236 | lstDecimal += 24
237 | }else if lstDecimal >= 24 {
238 |
239 | lstDecimal -= 24
240 | }
241 |
242 | let lstHMS = HMS.init(decimal: lstDecimal)
243 |
244 | return lstHMS
245 | }
246 |
247 |
248 | /// Converts the number of seconds in HH:MM:ss
249 | /// - Parameter seconds: Number of seconds that have to be converted
250 | /// - Returns: From value in input the equivalent in (hours,minute,seconds)
251 | public func secondsToHoursMinutesSeconds(_ seconds : Int) -> (Int,Int,Int) {
252 |
253 | return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
254 | }
255 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Resources/Settings.bundle/SunKit.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | FooterText
9 | Apache License
10 | Version 2.0, January 2004
11 | http://www.apache.org/licenses/
12 |
13 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
14 |
15 | 1. Definitions.
16 |
17 | "License" shall mean the terms and conditions for use, reproduction,
18 | and distribution as defined by Sections 1 through 9 of this document.
19 |
20 | "Licensor" shall mean the copyright owner or entity authorized by
21 | the copyright owner that is granting the License.
22 |
23 | "Legal Entity" shall mean the union of the acting entity and all
24 | other entities that control, are controlled by, or are under common
25 | control with that entity. For the purposes of this definition,
26 | "control" means (i) the power, direct or indirect, to cause the
27 | direction or management of such entity, whether by contract or
28 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
29 | outstanding shares, or (iii) beneficial ownership of such entity.
30 |
31 | "You" (or "Your") shall mean an individual or Legal Entity
32 | exercising permissions granted by this License.
33 |
34 | "Source" form shall mean the preferred form for making modifications,
35 | including but not limited to software source code, documentation
36 | source, and configuration files.
37 |
38 | "Object" form shall mean any form resulting from mechanical
39 | transformation or translation of a Source form, including but
40 | not limited to compiled object code, generated documentation,
41 | and conversions to other media types.
42 |
43 | "Work" shall mean the work of authorship, whether in Source or
44 | Object form, made available under the License, as indicated by a
45 | copyright notice that is included in or attached to the work
46 | (an example is provided in the Appendix below).
47 |
48 | "Derivative Works" shall mean any work, whether in Source or Object
49 | form, that is based on (or derived from) the Work and for which the
50 | editorial revisions, annotations, elaborations, or other modifications
51 | represent, as a whole, an original work of authorship. For the purposes
52 | of this License, Derivative Works shall not include works that remain
53 | separable from, or merely link (or bind by name) to the interfaces of,
54 | the Work and Derivative Works thereof.
55 |
56 | "Contribution" shall mean any work of authorship, including
57 | the original version of the Work and any modifications or additions
58 | to that Work or Derivative Works thereof, that is intentionally
59 | submitted to Licensor for inclusion in the Work by the copyright owner
60 | or by an individual or Legal Entity authorized to submit on behalf of
61 | the copyright owner. For the purposes of this definition, "submitted"
62 | means any form of electronic, verbal, or written communication sent
63 | to the Licensor or its representatives, including but not limited to
64 | communication on electronic mailing lists, source code control systems,
65 | and issue tracking systems that are managed by, or on behalf of, the
66 | Licensor for the purpose of discussing and improving the Work, but
67 | excluding communication that is conspicuously marked or otherwise
68 | designated in writing by the copyright owner as "Not a Contribution."
69 |
70 | "Contributor" shall mean Licensor and any individual or Legal Entity
71 | on behalf of whom a Contribution has been received by Licensor and
72 | subsequently incorporated within the Work.
73 |
74 | 2. Grant of Copyright License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | copyright license to reproduce, prepare Derivative Works of,
78 | publicly display, publicly perform, sublicense, and distribute the
79 | Work and such Derivative Works in Source or Object form.
80 |
81 | 3. Grant of Patent License. Subject to the terms and conditions of
82 | this License, each Contributor hereby grants to You a perpetual,
83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
84 | (except as stated in this section) patent license to make, have made,
85 | use, offer to sell, sell, import, and otherwise transfer the Work,
86 | where such license applies only to those patent claims licensable
87 | by such Contributor that are necessarily infringed by their
88 | Contribution(s) alone or by combination of their Contribution(s)
89 | with the Work to which such Contribution(s) was submitted. If You
90 | institute patent litigation against any entity (including a
91 | cross-claim or counterclaim in a lawsuit) alleging that the Work
92 | or a Contribution incorporated within the Work constitutes direct
93 | or contributory patent infringement, then any patent licenses
94 | granted to You under this License for that Work shall terminate
95 | as of the date such litigation is filed.
96 |
97 | 4. Redistribution. You may reproduce and distribute copies of the
98 | Work or Derivative Works thereof in any medium, with or without
99 | modifications, and in Source or Object form, provided that You
100 | meet the following conditions:
101 |
102 | (a) You must give any other recipients of the Work or
103 | Derivative Works a copy of this License; and
104 |
105 | (b) You must cause any modified files to carry prominent notices
106 | stating that You changed the files; and
107 |
108 | (c) You must retain, in the Source form of any Derivative Works
109 | that You distribute, all copyright, patent, trademark, and
110 | attribution notices from the Source form of the Work,
111 | excluding those notices that do not pertain to any part of
112 | the Derivative Works; and
113 |
114 | (d) If the Work includes a "NOTICE" text file as part of its
115 | distribution, then any Derivative Works that You distribute must
116 | include a readable copy of the attribution notices contained
117 | within such NOTICE file, excluding those notices that do not
118 | pertain to any part of the Derivative Works, in at least one
119 | of the following places: within a NOTICE text file distributed
120 | as part of the Derivative Works; within the Source form or
121 | documentation, if provided along with the Derivative Works; or,
122 | within a display generated by the Derivative Works, if and
123 | wherever such third-party notices normally appear. The contents
124 | of the NOTICE file are for informational purposes only and
125 | do not modify the License. You may add Your own attribution
126 | notices within Derivative Works that You distribute, alongside
127 | or as an addendum to the NOTICE text from the Work, provided
128 | that such additional attribution notices cannot be construed
129 | as modifying the License.
130 |
131 | You may add Your own copyright statement to Your modifications and
132 | may provide additional or different license terms and conditions
133 | for use, reproduction, or distribution of Your modifications, or
134 | for any such Derivative Works as a whole, provided Your use,
135 | reproduction, and distribution of the Work otherwise complies with
136 | the conditions stated in this License.
137 |
138 | 5. Submission of Contributions. Unless You explicitly state otherwise,
139 | any Contribution intentionally submitted for inclusion in the Work
140 | by You to the Licensor shall be under the terms and conditions of
141 | this License, without any additional terms or conditions.
142 | Notwithstanding the above, nothing herein shall supersede or modify
143 | the terms of any separate license agreement you may have executed
144 | with Licensor regarding such Contributions.
145 |
146 | 6. Trademarks. This License does not grant permission to use the trade
147 | names, trademarks, service marks, or product names of the Licensor,
148 | except as required for reasonable and customary use in describing the
149 | origin of the Work and reproducing the content of the NOTICE file.
150 |
151 | 7. Disclaimer of Warranty. Unless required by applicable law or
152 | agreed to in writing, Licensor provides the Work (and each
153 | Contributor provides its Contributions) on an "AS IS" BASIS,
154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
155 | implied, including, without limitation, any warranties or conditions
156 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
157 | PARTICULAR PURPOSE. You are solely responsible for determining the
158 | appropriateness of using or redistributing the Work and assume any
159 | risks associated with Your exercise of permissions under this License.
160 |
161 | 8. Limitation of Liability. In no event and under no legal theory,
162 | whether in tort (including negligence), contract, or otherwise,
163 | unless required by applicable law (such as deliberate and grossly
164 | negligent acts) or agreed to in writing, shall any Contributor be
165 | liable to You for damages, including any direct, indirect, special,
166 | incidental, or consequential damages of any character arising as a
167 | result of this License or out of the use or inability to use the
168 | Work (including but not limited to damages for loss of goodwill,
169 | work stoppage, computer failure or malfunction, or any and all
170 | other commercial damages or losses), even if such Contributor
171 | has been advised of the possibility of such damages.
172 |
173 | 9. Accepting Warranty or Additional Liability. While redistributing
174 | the Work or Derivative Works thereof, You may choose to offer,
175 | and charge a fee for, acceptance of support, warranty, indemnity,
176 | or other liability obligations and/or rights consistent with this
177 | License. However, in accepting such obligations, You may act only
178 | on Your own behalf and on Your sole responsibility, not on behalf
179 | of any other Contributor, and only if You agree to indemnify,
180 | defend, and hold each Contributor harmless for any liability
181 | incurred by, or claims asserted against, such Contributor by reason
182 | of your accepting any such warranty or additional liability.
183 |
184 | END OF TERMS AND CONDITIONS
185 |
186 | APPENDIX: How to apply the Apache License to your work.
187 |
188 | To apply the Apache License to your work, attach the following
189 | boilerplate notice, with the fields enclosed by brackets "[]"
190 | replaced with your own identifying information. (Don't include
191 | the brackets!) The text should be enclosed in the appropriate
192 | comment syntax for the file format. We also recommend that a
193 | file or class name and description of purpose be included on the
194 | same "printed page" as the copyright notice for easier
195 | identification within third-party archives.
196 |
197 | Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
198 |
199 | Licensed under the Apache License, Version 2.0 (the "License");
200 | you may not use this file except in compliance with the License.
201 | You may obtain a copy of the License at
202 |
203 | http://www.apache.org/licenses/LICENSE-2.0
204 |
205 | Unless required by applicable law or agreed to in writing, software
206 | distributed under the License is distributed on an "AS IS" BASIS,
207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
208 | See the License for the specific language governing permissions and
209 | limitations under the License.
210 |
211 | Type
212 | PSGroupSpecifier
213 |
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/Tests/SunKitTests/UT_Sun.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UT_Sun.swift
3 | //
4 | //
5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | import XCTest
20 | import Foundation
21 | import CoreLocation
22 | @testable import SunKit
23 |
24 | final class UT_Sun: XCTestCase {
25 |
26 | /*--------------------------------------------------------------------
27 | Thresholds. UTs will pass if |output - expectedOutput| < threshold
28 | *-------------------------------------------------------------------*/
29 | static let sunAzimuthThreshold: Double = 0.05
30 | static let sunAltitudeThreshold: Double = 0.1
31 | static let sunSetRiseThresholdInSeconds: Double = 120 //2 minutes in seconds
32 | static let sunEquinoxesAndSolsticesThresholdInSeconds: Double = 700 // approxametly 11 minutes
33 |
34 | static let objectShadowThreshold: Double = 0.01
35 |
36 | /*--------------------------------------------------------------------
37 | Naples timezone and location
38 | *-------------------------------------------------------------------*/
39 | static let naplesLocation: CLLocation = .init(latitude: 40.84014, longitude: 14.25226)
40 | static let timeZoneNaples = 1
41 | static let timeZoneNaplesDaylightSaving = 2
42 |
43 | /*--------------------------------------------------------------------
44 | Tokyo timezone and location
45 | *-------------------------------------------------------------------*/
46 | static let tokyoLocation: CLLocation = .init(latitude: 35.68946, longitude: 139.69172)
47 | static let timeZoneTokyo = 9
48 |
49 | /*--------------------------------------------------------------------
50 | Louisa USA timezone and location
51 | *-------------------------------------------------------------------*/
52 | static let louisaLocation: CLLocation = .init(latitude: 38, longitude: -78)
53 | static let timeZoneLouisa = -5
54 | static let timeZoneLouisaDaylightSaving = -4
55 |
56 | /*--------------------------------------------------------------------
57 | Tromso circumpolar timezone and location
58 | *-------------------------------------------------------------------*/
59 | static let tromsoLocation: CLLocation = .init(latitude: 69.6489, longitude: 18.95508)
60 | static let timeZoneTromso = 1
61 | static let timeZoneTromsoDaylightSaving = 2
62 |
63 | /*--------------------------------------------------------------------
64 | Mumbai timezone and location
65 | *-------------------------------------------------------------------*/
66 | static let mumbaiLocation: CLLocation = .init(latitude: 18.94017, longitude: 72.83489)
67 | static let timeZoneMumbai = 5.5
68 |
69 |
70 | /// Test of Sun azimuth, sunrise, sunset, evening golden hour start and evening golden hour end
71 | /// Value for expected results have been taken from SunCalc.org
72 | func testOfSun() throws {
73 |
74 | /*--------------------------------------------------------------------
75 | Naples
76 | *-------------------------------------------------------------------*/
77 |
78 | //Test1: 19/11/22 20:00. Timezone +1.
79 |
80 | //Step1: Creating Sun instance in Naples and with timezone +1
81 | var timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current
82 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest)
83 |
84 | //Step2: Setting 19/11/22 20:00 as date. (No daylight saving)
85 | var dateUnderTest = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00,timeZone: timeZoneUnderTest)
86 | sunUnderTest.setDate(dateUnderTest)
87 |
88 | //Step3: Saving expected outputs
89 | var expectedAzimuth = 275.84
90 | var expectedAltitude = -37.34
91 |
92 | var expectedSunRise = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 6, minute: 54, seconds: 12,timeZone: timeZoneUnderTest)
93 | var expectedSunset = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 42, seconds: 07,timeZone: timeZoneUnderTest)
94 |
95 | var expectedGoldenHourStart = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 00, seconds: 00,timeZone: timeZoneUnderTest)
96 | var expectedGoldenHourEnd = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 59, seconds: 00,timeZone: timeZoneUnderTest)
97 |
98 | var expectedcivilDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 6, minute: 24, seconds: 51,timeZone: timeZoneUnderTest)
99 | var expectedcivilDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 17, minute: 11, seconds: 28,timeZone: timeZoneUnderTest)
100 |
101 | var expectedSolarNoon = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 11, minute: 48, seconds: 21,timeZone: timeZoneUnderTest)
102 |
103 | let expectednauticalDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 5, minute: 52, seconds: 21,timeZone: timeZoneUnderTest)
104 |
105 | let expectednauticalDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 17, minute: 44, seconds: 45,timeZone: timeZoneUnderTest)
106 |
107 | let expectedastronomicalDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 5, minute: 19, seconds: 25,timeZone: timeZoneUnderTest)
108 |
109 | let expectedastronomicalDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 18, minute: 17, seconds: 20,timeZone: timeZoneUnderTest)
110 |
111 |
112 | //Step4: Check if the output are close to the expected ones
113 |
114 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
115 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
116 |
117 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
118 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
119 |
120 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
121 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
122 |
123 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
124 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
125 |
126 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
127 |
128 | XCTAssertTrue(abs(expectednauticalDusk.timeIntervalSince1970 - sunUnderTest.nauticalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
129 | XCTAssertTrue(abs(expectednauticalDawn.timeIntervalSince1970 - sunUnderTest.nauticalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
130 |
131 | XCTAssertTrue(abs(expectedastronomicalDusk.timeIntervalSince1970 - sunUnderTest.astronomicalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
132 |
133 | XCTAssertTrue(abs(expectedastronomicalDawn.timeIntervalSince1970 - sunUnderTest.astronomicalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
134 |
135 |
136 | //Test: 31/12/2024 15:32. Timezone +1. Leap Year.
137 |
138 | //Step1: Creating Sun instance in Naples and with timezone +1
139 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current
140 |
141 | sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest)
142 |
143 | //Step2: Setting 31/12/2024 15:32 as date. (No daylight saving)
144 | dateUnderTest = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 15, minute: 32, seconds: 00,timeZone: timeZoneUnderTest)
145 | sunUnderTest.setDate(dateUnderTest)
146 |
147 | //Step3: Saving expected outputs
148 | expectedAzimuth = 226.99
149 | expectedAltitude = 10.35
150 |
151 | expectedSunRise = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 7, minute: 26, seconds: 57,timeZone: timeZoneUnderTest)
152 | expectedSunset = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 16, minute: 45, seconds: 32,timeZone: timeZoneUnderTest)
153 |
154 | expectedGoldenHourStart = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 16, minute: 02, seconds: 00,timeZone: timeZoneUnderTest)
155 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 17, minute: 05, seconds: 00,timeZone: timeZoneUnderTest)
156 |
157 | expectedcivilDawn = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 6, minute: 56, seconds: 24,timeZone: timeZoneUnderTest)
158 | expectedcivilDusk = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 17, minute: 16, seconds: 06,timeZone: timeZoneUnderTest)
159 |
160 | expectedSolarNoon = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 12, minute: 06, seconds: 11,timeZone: timeZoneUnderTest)
161 |
162 | //Step4: Check if the output are close to the expected ones
163 |
164 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
165 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
166 |
167 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
168 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
169 |
170 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
171 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
172 |
173 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
174 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
175 |
176 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
177 |
178 |
179 | /*--------------------------------------------------------------------
180 | Tokyo
181 | *-------------------------------------------------------------------*/
182 |
183 | //Test: 1/08/22 16:50. Timezone +9.
184 |
185 | //Step1: Creating sun instance in Tokyo and with timezone +9
186 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneTokyo * Int(SECONDS_IN_ONE_HOUR)) ?? .current
187 | sunUnderTest = Sun.init(location: UT_Sun.tokyoLocation, timeZone: timeZoneUnderTest)
188 |
189 | //Step2: Setting 1/08/22 16:50 as date.
190 | dateUnderTest = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 16, minute: 50, seconds: 00,timeZone: timeZoneUnderTest)
191 | sunUnderTest.setDate(dateUnderTest)
192 |
193 | //Step3: Saving expected outputs
194 | expectedAzimuth = 276.98
195 | expectedAltitude = 21.90
196 |
197 | expectedSunRise = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 4, minute: 48, seconds: 29,timeZone: timeZoneUnderTest)
198 | expectedSunset = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 18, minute: 46, seconds: 15,timeZone: timeZoneUnderTest)
199 |
200 | expectedGoldenHourStart = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 18, minute: 11, seconds: 00,timeZone: timeZoneUnderTest)
201 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 19, minute: 04, seconds: 00,timeZone: timeZoneUnderTest)
202 |
203 | expectedcivilDawn = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 4, minute: 20, seconds: 39,timeZone: timeZoneUnderTest)
204 | expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 19, minute: 14, seconds: 00,timeZone: timeZoneUnderTest)
205 |
206 | expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 11, minute: 47, seconds: 36,timeZone: timeZoneUnderTest)
207 |
208 | //Step4: Check if the output are close to the expected ones
209 |
210 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
211 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
212 |
213 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
214 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
215 |
216 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
217 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
218 |
219 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
220 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
221 |
222 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
223 |
224 | /*--------------------------------------------------------------------
225 | Louisa USA
226 | *-------------------------------------------------------------------*/
227 |
228 | //Test: 1/01/15 22:00. Timezone -5.
229 |
230 | //Step1: Creating sun instance in Louisa and with timezone -5 (No daylight saving)
231 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneLouisa * Int(SECONDS_IN_ONE_HOUR)) ?? .current
232 | sunUnderTest = Sun.init(location: UT_Sun.louisaLocation, timeZone: timeZoneUnderTest)
233 |
234 | //Step2: Setting 1/01/15 22:00 as date. (No daylight saving)
235 | dateUnderTest = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 22, minute: 00, seconds: 00,timeZone: timeZoneUnderTest)
236 | sunUnderTest.setDate(dateUnderTest)
237 |
238 | //Step3: Saving expected outputs
239 | expectedAzimuth = 287.62
240 | expectedAltitude = -57.41
241 |
242 | expectedSunRise = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 7, minute: 27, seconds: 29,timeZone: timeZoneUnderTest)
243 | expectedSunset = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 03, seconds: 25,timeZone: timeZoneUnderTest)
244 |
245 | expectedGoldenHourStart = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 16, minute: 22, seconds: 00,timeZone: timeZoneUnderTest)
246 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 22, seconds: 00,timeZone: timeZoneUnderTest)
247 |
248 | expectedcivilDawn = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 6, minute: 58, seconds: 28,timeZone: timeZoneUnderTest)
249 | expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 32, seconds: 27,timeZone: timeZoneUnderTest)
250 |
251 | expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 12, minute: 15, seconds: 23,timeZone: timeZoneUnderTest)
252 |
253 | //Step4: Check if the output are close to the expected ones
254 |
255 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
256 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
257 |
258 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
259 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
260 |
261 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
262 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
263 |
264 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
265 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
266 |
267 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
268 |
269 | /*--------------------------------------------------------------------
270 | Tromso circumpolar
271 | *-------------------------------------------------------------------*/
272 |
273 | //Test: 19/01/22 17:31. Timezone +1.
274 |
275 | //Step1: Creating sun instance in Tromso and with timezone +1 (No daylight saving)
276 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneTromso * Int(SECONDS_IN_ONE_HOUR)) ?? .current
277 | sunUnderTest = Sun.init(location: UT_Sun.tromsoLocation, timeZone: timeZoneUnderTest)
278 |
279 | //Step2: Setting 19/01/22 17:31 as date. (No daylight saving)
280 | dateUnderTest = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 17, minute: 31, seconds: 00,timeZone: timeZoneUnderTest)
281 | sunUnderTest.setDate(dateUnderTest)
282 |
283 | //Step3: Saving expected outputs
284 | expectedAzimuth = 257.20
285 | expectedAltitude = -16.93
286 |
287 | expectedSunRise = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 10, minute: 41, seconds: 46,timeZone: timeZoneUnderTest)
288 | expectedSunset = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 13, minute: 08, seconds: 48,timeZone: timeZoneUnderTest)
289 | expectedcivilDawn = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 08, minute: 45, seconds: 30,timeZone: timeZoneUnderTest)
290 | expectedcivilDusk = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 15, minute: 05, seconds: 08,timeZone: timeZoneUnderTest)
291 |
292 | expectedSolarNoon = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 11, minute: 54, seconds: 52,timeZone: timeZoneUnderTest)
293 |
294 | //Step4: Check if the output are close to the expected ones
295 |
296 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
297 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
298 |
299 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
300 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
301 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
302 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
303 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
304 |
305 |
306 | /*--------------------------------------------------------------------
307 | Mumbai
308 | *-------------------------------------------------------------------*/
309 |
310 | //Test: 12/03/23 14:46. Timezone +5.5.
311 |
312 | //Step1: Creating Sun instance in Mumbai and with timezone +5.5
313 | timeZoneUnderTest = .init(secondsFromGMT: Int(UT_Sun.timeZoneMumbai * SECONDS_IN_ONE_HOUR)) ?? .current
314 | sunUnderTest = Sun.init(location: UT_Sun.mumbaiLocation, timeZone: timeZoneUnderTest)
315 |
316 | //Step2: Setting 12/03/23 14:46 as date.
317 | dateUnderTest = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 14, minute: 46, seconds: 00,timeZone: timeZoneUnderTest)
318 | sunUnderTest.setDate(dateUnderTest)
319 |
320 | //Step3: Saving expected outputs
321 | expectedAzimuth = 235.41
322 | expectedAltitude = 53.51
323 |
324 | expectedSunRise = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 6, minute: 49, seconds: 35,timeZone: timeZoneUnderTest)
325 | expectedSunset = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 18, minute: 47, seconds: 42,timeZone: timeZoneUnderTest)
326 | expectedcivilDawn = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 6, minute: 27, seconds: 59,timeZone: timeZoneUnderTest)
327 | expectedcivilDusk = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 19, minute: 09, seconds: 19,timeZone: timeZoneUnderTest)
328 |
329 | expectedSolarNoon = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 12, minute: 48, seconds: 31,timeZone: timeZoneUnderTest)
330 |
331 | //Step4: Check if the output are close to the expected ones
332 |
333 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold)
334 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold)
335 |
336 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
337 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
338 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
339 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
340 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds)
341 |
342 |
343 | //Test for issue #26 in github
344 | guard let pst = TimeZone(abbreviation: "PST") else {
345 | abort()
346 | }
347 |
348 | guard let utc = TimeZone(abbreviation: "UTC") else {
349 | abort()
350 | }
351 |
352 | let location: CLLocation = .init(latitude: 34.052235, longitude: -118.243683)
353 |
354 | var sun = Sun.init(location: location, timeZone: pst)
355 |
356 | sun.setDate(SunKit.createDateCustomTimeZone(day: 11, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst))
357 | XCTAssertEqual(sun.sunrise.toString(pst), "03/11, 06:08")
358 | XCTAssertEqual(sun.sunset.toString(pst), "03/11, 17:58")
359 | XCTAssertEqual(sun.sunrise.toString(utc), "03/11, 14:08")
360 | XCTAssertEqual(sun.sunset.toString(utc), "03/12, 01:58")
361 |
362 | sun.setDate(SunKit.createDateCustomTimeZone(day: 13, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst))
363 | XCTAssertEqual(sun.sunrise.toString(pst), "03/13, 07:06")
364 | XCTAssertEqual(sun.sunset.toString(pst), "03/13, 18:59")
365 | XCTAssertEqual(sun.sunrise.toString(utc), "03/13, 14:06")
366 | XCTAssertEqual(sun.sunset.toString(utc), "03/14, 01:59")
367 |
368 | }
369 |
370 | func testOfObjectShadow() throws{
371 |
372 | //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving)
373 | let timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current
374 | // let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current
375 | let sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest)
376 |
377 |
378 |
379 | XCTAssertTrue(abs(sunUnderTest.shadowLength(with: .init(degrees: 43.40))! - 1.06 ) <= UT_Sun.objectShadowThreshold)
380 |
381 | XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: -0.01)) == nil)
382 |
383 | XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: 90)) == 0)
384 |
385 |
386 | }
387 |
388 | func testOfEquinoxesAndSolstices() throws {
389 |
390 | //Test: 19/01/22 17:31. Timezone +1. Naples
391 |
392 | //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving)
393 | let timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current
394 | let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current
395 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest)
396 |
397 | //Step2: Setting 19/01/22 17:31 as date. (No daylight saving)
398 | let dateUnderTest = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 17, minute: 31, seconds: 00,timeZone: timeZoneUnderTest)
399 | sunUnderTest.setDate(dateUnderTest)
400 |
401 | //Step3: Saving expected outputs
402 |
403 | let expectedMarchEquinox = createDateCustomTimeZone(day: 20, month: 3, year: 2022, hour: 16, minute: 33, seconds: 00,timeZone: timeZoneUnderTest)
404 | let expectedJuneSolstice = createDateCustomTimeZone(day: 21, month: 6, year: 2022, hour: 11, minute: 13, seconds: 00,timeZone: timeZoneDaylightSaving)
405 | let expectedSeptemberEquinox = createDateCustomTimeZone(day: 23, month: 09, year: 2022, hour: 03, minute: 03, seconds: 00,timeZone: timeZoneDaylightSaving)
406 | let expectedDecemberSolstice = createDateCustomTimeZone(day: 21, month: 12, year: 2022, hour: 22, minute: 47, seconds: 00,timeZone: timeZoneUnderTest)
407 |
408 | //Step4: Check if the output are close to the expected ones
409 | XCTAssertTrue(abs(expectedMarchEquinox.timeIntervalSince1970 - sunUnderTest.marchEquinox.timeIntervalSince1970)
410 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds)
411 |
412 | XCTAssertTrue(abs(expectedJuneSolstice.timeIntervalSince1970 - sunUnderTest.juneSolstice.timeIntervalSince1970)
413 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds)
414 |
415 | XCTAssertTrue(abs(expectedSeptemberEquinox.timeIntervalSince1970 - sunUnderTest.septemberEquinox.timeIntervalSince1970)
416 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds)
417 |
418 | XCTAssertTrue(abs(expectedDecemberSolstice.timeIntervalSince1970 - sunUnderTest.decemberSolstice.timeIntervalSince1970)
419 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds)
420 |
421 | }
422 |
423 |
424 | /// Test of a sun ionstance whenm you play with timezones and change location
425 | func testOfSunWhenTimezoneChanges() throws {
426 |
427 | //Step1: Creating Sun instance in Naples and with timezone +1
428 | let timeZoneNaples: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current
429 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneNaples)
430 |
431 | //Step2: Setting 19/11/22 20:00 as date. (No daylight saving)
432 | let dateUnderTest = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00,timeZone: timeZoneNaples)
433 | sunUnderTest.setDate(dateUnderTest)
434 |
435 | //Step3: Change location and timezone
436 | let timeZoneTokyo: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneTokyo * Int(SECONDS_IN_ONE_HOUR)) ?? .current
437 | sunUnderTest.setLocation(UT_Sun.tokyoLocation, timeZoneTokyo)
438 |
439 | //Step4: Saving expected outputs for all the date
440 | let expectedDate = createDateCustomTimeZone(day: 20, month: 11, year: 2022, hour: 4, minute: 00, seconds: 00,timeZone: timeZoneTokyo)
441 |
442 | //Step5: Check if output of sunUnderTest.date matches the expected output
443 | XCTAssertTrue(expectedDate == sunUnderTest.date)
444 |
445 | }
446 |
447 |
448 |
449 | func testPerformance() throws {
450 | // Performance of setDate function that will refresh all the sun variables
451 |
452 | //Step1: Creating sun instance in Naples with timezone +1
453 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: Double(UT_Sun.timeZoneNaples))
454 |
455 | //Step2: Setting 19/11/22 20:00 as date.
456 | let dateUnderTest = createDateCurrentTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00)
457 |
458 | self.measure {
459 | sunUnderTest.setDate(dateUnderTest)
460 | }
461 | }
462 | }
463 |
--------------------------------------------------------------------------------
/Sources/SunKit/Sun.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sun.swift
3 | //
4 | //
5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano
6 | //
7 | // Licensed under the Apache License, Version 2.0 (the "License");
8 | // you may not use this file except in compliance with the License.
9 | // You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing, software
14 | // distributed under the License is distributed on an "AS IS" BASIS,
15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | // See the License for the specific language governing permissions and
17 | // limitations under the License.
18 |
19 | import Foundation
20 | import CoreLocation
21 |
22 | public struct Sun: Identifiable, Sendable {
23 | public let id: UUID = UUID()
24 |
25 | /*--------------------------------------------------------------------
26 | Public get Variables
27 | *-------------------------------------------------------------------*/
28 |
29 | public private(set) var location: CLLocation
30 | public private(set) var timeZone: TimeZone
31 | public private(set) var date: Date
32 |
33 | /*--------------------------------------------------------------------
34 | Sun Events during the day
35 | *-------------------------------------------------------------------*/
36 |
37 | ///Date of Sunrise
38 | public private(set) var sunrise: Date = Date()
39 | ///Date of Sunset
40 | public private(set) var sunset: Date = Date()
41 | ///Date of Solar Noon for
42 | public private(set) var solarNoon: Date = Date()
43 | ///Date of Solar Midnight
44 | public private(set) var solarMidnight: Date = Date()
45 |
46 | ///Date at which evening evening Golden hour starts
47 | public private(set) var eveningGoldenHourStart: Date = Date()
48 | ///Date at which evening evening Golden hour ends
49 | public private(set) var eveningGoldenHourEnd: Date = Date()
50 |
51 | ///Date at which evening Morning Golden hour starts
52 | public private(set) var morningGoldenHourStart: Date = Date()
53 | ///Date at which evening Morning Golden hour ends
54 | public private(set) var morningGoldenHourEnd: Date = Date()
55 |
56 |
57 | ///Date at which there is the Civil Dusk
58 | public private(set) var civilDusk: Date = Date()
59 | ///Date at which there is the Civil Dawn
60 | public private(set) var civilDawn: Date = Date()
61 |
62 | ///Date at which there is the Nautical Dusk
63 | public private(set) var nauticalDusk: Date = Date()
64 | ///Date at which there is the Nautical Dawn
65 | public private(set) var nauticalDawn: Date = Date()
66 |
67 | ///Date at which there is the Astronomical Dusk
68 | public private(set) var astronomicalDusk: Date = Date()
69 | ///Date at which there is the Astronomical Dawn
70 | public private(set) var astronomicalDawn: Date = Date()
71 |
72 | ///Date at which morning Blue Hour starts. Sun at -6 degrees elevation = civil dusk
73 | public var morningBlueHourStart: Date{
74 | civilDawn
75 | }
76 |
77 | ///Date at which morning Blue Hour ends. Sun at -4 degrees elevation = morning golden hour start
78 | public var morningBlueHourEnd: Date {
79 | morningGoldenHourStart
80 | }
81 |
82 | ///Date at which evening Blue Hour starts. Sun at -4 degrees elevation = evening golden hour end
83 | public var eveningBlueHourStart: Date{
84 | eveningGoldenHourEnd
85 | }
86 |
87 | ///Date at which morning Blue Hour ends. Sun at -6 degrees elevation = Civil Dawn
88 | public var eveningBlueHourEnd: Date {
89 | civilDusk
90 | }
91 |
92 |
93 | /*--------------------------------------------------------------------
94 | Sun Azimuths for Self.date and for Sunrise,Sunset and Solar Noon
95 | *-------------------------------------------------------------------*/
96 |
97 | ///Azimuth of Sunrise
98 | public private(set) var sunriseAzimuth: Double = 0
99 | ///Azimuth of Sunset
100 | public private(set) var sunsetAzimuth: Double = 0
101 | ///Azimuth of Solar noon
102 | public private(set) var solarNoonAzimuth: Double = 0
103 |
104 | // Sun azimuth for (Location,Date) in Self
105 | public var azimuth: Angle {
106 | sunHorizonCoordinates.azimuth
107 | }
108 |
109 | // Sun altitude for (Location,Date) in Self
110 | public var altitude: Angle {
111 | sunHorizonCoordinates.altitude
112 | }
113 |
114 | public private(set) var sunEquatorialCoordinates: EquatorialCoordinates = .init(declination: .zero)
115 | public private(set) var sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: .zero)
116 |
117 | /*--------------------------------------------------------------------
118 | Sun Events during the year
119 | *-------------------------------------------------------------------*/
120 |
121 | ///Date at which there will be march equinox
122 | public private(set) var marchEquinox: Date = Date()
123 | ///Date at which there will be june solstice
124 | public private(set) var juneSolstice: Date = Date()
125 | ///Date at which there will be september solstice
126 | public private(set) var septemberEquinox: Date = Date()
127 | ///Date at which there will be december solstice
128 | public private(set) var decemberSolstice: Date = Date()
129 |
130 | /*--------------------------------------------------------------------
131 | Nice To Have public variables
132 | *-------------------------------------------------------------------*/
133 |
134 | /// Longitude of location
135 | public var longitude: Angle {
136 | .init(degrees: location.coordinate.longitude)
137 | }
138 |
139 | /// Latitude of Location
140 | public var latitude: Angle {
141 | .init(degrees: location.coordinate.latitude)
142 | }
143 |
144 | /// Returns daylight time in seconds
145 | public var totalDayLightTime: Int {
146 | let diffComponents = calendar.dateComponents([.second], from: sunrise, to: sunset)
147 |
148 | return diffComponents.second ?? 0
149 | }
150 |
151 | /// Returns night time in seconds
152 | public var totalNightTime: Int {
153 | let startOfTheDay = calendar.startOfDay(for: date)
154 | let endOfTheDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: startOfTheDay)!
155 | var diffComponents = calendar.dateComponents([.second], from: startOfTheDay, to: sunrise)
156 | var nightHours: Int = diffComponents.second ?? 0
157 | diffComponents = calendar.dateComponents([.second], from: sunset, to: endOfTheDay)
158 | nightHours = nightHours + (diffComponents.second ?? 0)
159 |
160 | return nightHours
161 | }
162 |
163 | /// Returns True if is night
164 | public var isNight: Bool {
165 | if !isCircumPolar {
166 | date < sunrise || date > sunset
167 | } else {
168 | isAlwaysNight
169 | }
170 | }
171 |
172 | /// Returns True if is twilight time
173 | public var isTwilight: Bool {
174 | (astronomicalDawn <= date && date < sunrise) || (sunset < date && date <= astronomicalDusk)
175 | }
176 |
177 | /// Returns True if we are in evening golden hour range
178 | public var isEveningGoldenHour: Bool {
179 | date >= eveningGoldenHourStart && date <= eveningGoldenHourEnd
180 | }
181 |
182 | /// Returns True if we are in morning golden hour range
183 | public var isMorningGoldenHour: Bool {
184 | date >= morningGoldenHourStart && date <= morningGoldenHourEnd
185 | }
186 |
187 | /// Returns True if we are in golden hour range
188 | public var isGoldenHour: Bool {
189 | isMorningGoldenHour || isEveningGoldenHour
190 | }
191 |
192 | /// Returns True if we are in evening blue hour range
193 | public var isEveningBlueHour: Bool {
194 | date >= eveningBlueHourStart && date <= eveningBlueHourEnd
195 | }
196 |
197 | /// Returns True if we are in morning blue hour range
198 | public var isMorningBlueHour: Bool {
199 | date >= morningBlueHourStart && date <= morningBlueHourEnd
200 | }
201 |
202 | /// Returns True if we are in blue hour range
203 | public var isBlueHour: Bool {
204 | isMorningBlueHour || isEveningBlueHour
205 | }
206 |
207 |
208 | /// Returns true if we are near the pole and we are in a situation in which Sun Events during the day could have no meaning
209 | public var isCircumPolar: Bool {
210 | isAlwaysDay || isAlwaysNight
211 | }
212 |
213 | /// Returns true if for (Location,Date) is always daylight (e.g Tromso city in summer)
214 | public var isAlwaysDay: Bool {
215 | let startOfTheDay = calendar.startOfDay(for: date)
216 | let almostNextDay = startOfTheDay + Double(SECONDS_IN_ONE_DAY)
217 |
218 | return sunset == almostNextDay || sunrise < startOfTheDay + SECONDS_IN_TEN_MINUTES
219 | }
220 |
221 | /// Returns true if for (Location,Date) is always daylight (e.g Tromso city in Winter)
222 | public var isAlwaysNight: Bool {
223 | sunset - TWO_HOURS_IN_SECONDS < sunrise
224 | }
225 |
226 | /*--------------------------------------------------------------------
227 | Initializers
228 | *-------------------------------------------------------------------*/
229 |
230 | public init(location: CLLocation, timeZone: TimeZone, date: Date = Date()) {
231 | self.timeZone = timeZone
232 | self.location = location
233 | self.date = date
234 |
235 | refresh()
236 | }
237 |
238 | init(location: CLLocation, timeZone: Double, date: Date = Date()) {
239 | let timeZoneSeconds: Int = Int(timeZone * SECONDS_IN_ONE_HOUR)
240 | self.timeZone = TimeZone.init(secondsFromGMT: timeZoneSeconds) ?? .current
241 | self.location = location
242 | self.date = date
243 |
244 | refresh()
245 | }
246 |
247 | /*--------------------------------------------------------------------
248 | Public methods
249 | *-------------------------------------------------------------------*/
250 |
251 | /*--------------------------------------------------------------------
252 | Changing date of interest
253 | *-------------------------------------------------------------------*/
254 |
255 | public mutating func setDate(_ newDate: Date) {
256 | let newDay = calendar.dateComponents([.day,.month,.year], from: newDate)
257 | let oldDay = calendar.dateComponents([.day,.month,.year], from: date)
258 |
259 | let isSameDay: Bool = (newDay == oldDay)
260 | date = newDate
261 |
262 | refresh(needToComputeSunEvents: !isSameDay) //If is the same day no need to compute again Daily Sun Events
263 | }
264 |
265 | /*--------------------------------------------------------------------
266 | Changing Location
267 | *-------------------------------------------------------------------*/
268 |
269 |
270 | /// Changing location and timezone
271 | /// - Parameters:
272 | /// - newLocation: New location
273 | /// - newTimeZone: New timezone for the given location. Is highly recommanded to pass a Timezone initialized via .init(identifier: ) method
274 | public mutating func setLocation(_ newLocation: CLLocation,_ newTimeZone: TimeZone) {
275 | timeZone = newTimeZone
276 | location = newLocation
277 | refresh()
278 | }
279 |
280 | /// Changing only the location
281 | /// - Parameter newLocation: New Location
282 | public mutating func setLocation(_ newLocation: CLLocation) {
283 | location = newLocation
284 | refresh()
285 | }
286 |
287 |
288 | /// Is highly recommanded to use the other method to change both location and timezone. This will be kept only for backwards retrocompatibility.
289 | /// - Parameters:
290 | /// - newLocation: New Location
291 | /// - newTimeZone: New Timezone express in Double. For timezones which differs of half an hour add 0.5,
292 | public mutating func setLocation(_ newLocation: CLLocation,_ newTimeZone: Double) {
293 | let timeZoneSeconds: Int = Int(newTimeZone * SECONDS_IN_ONE_HOUR)
294 | timeZone = TimeZone(secondsFromGMT: timeZoneSeconds) ?? .current
295 | location = newLocation
296 | refresh()
297 | }
298 |
299 |
300 | /*--------------------------------------------------------------------
301 | Changing Timezone
302 | *-------------------------------------------------------------------*/
303 |
304 | /// Changing only the timezone.
305 | /// - Parameter newTimeZone: New Timezone
306 | public mutating func setTimeZone(_ newTimeZone: TimeZone) {
307 | timeZone = newTimeZone
308 | refresh()
309 | }
310 |
311 | /// Is highly recommanded to use the other method to change timezone. This will be kept only for backwards retrocompatibility.
312 | /// - Parameter newTimeZone: New Timezone express in Double. For timezones which differs of half an hour add 0.5,
313 | public mutating func setTimeZone(_ newTimeZone: Double) {
314 | let timeZoneSeconds: Int = Int(newTimeZone * SECONDS_IN_ONE_HOUR)
315 | timeZone = TimeZone(secondsFromGMT: timeZoneSeconds) ?? .current
316 | refresh()
317 | }
318 |
319 | /*--------------------------------------------------------------------
320 | Debug functions
321 | *-------------------------------------------------------------------*/
322 |
323 | /// Dumps all the Sun Events dates
324 | public func dumpDateInfos(){
325 |
326 | print("Current Date -> \(dateFormatter.string(from: date))")
327 | print("Sunrise -> \(dateFormatter.string(from: sunrise))")
328 | print("Sunset -> \(dateFormatter.string(from: sunset))")
329 | print("Solar Noon -> \(dateFormatter.string(from: solarNoon))")
330 | print("Solar Midnight -> \(dateFormatter.string(from: solarMidnight))")
331 | print("Evening Golden Hour Start -> \(dateFormatter.string(from: eveningGoldenHourStart))")
332 | print("Evening Golden Hour End -> \(dateFormatter.string(from: eveningGoldenHourEnd))")
333 | print("Morning Golden Hour Start -> \(dateFormatter.string(from: morningGoldenHourStart))")
334 | print("Morning Golden Hour End -> \(dateFormatter.string(from: morningGoldenHourEnd))")
335 | print("Civil dusk -> \(dateFormatter.string(from: civilDusk))")
336 | print("Civil Dawn -> \(dateFormatter.string(from: civilDawn))")
337 | print("Nautical Dusk -> \(dateFormatter.string(from: nauticalDusk))")
338 | print("Nautical Dawn -> \(dateFormatter.string(from: nauticalDawn))")
339 | print("Astronomical Dusk -> \(dateFormatter.string(from: astronomicalDusk))")
340 | print("Astronomical Dawn -> \(dateFormatter.string(from: astronomicalDawn))")
341 | print("Morning Blue Hour Start -> \(dateFormatter.string(from: morningBlueHourStart))")
342 | print("Morning Blue Hour End -> \(dateFormatter.string(from: morningBlueHourEnd))")
343 | print("evening Blue Hour Start -> \(dateFormatter.string(from: eveningBlueHourStart))")
344 | print("evening Blue Hour End -> \(dateFormatter.string(from: eveningBlueHourEnd))")
345 |
346 | print("March Equinox -> \(dateFormatter.string(from: marchEquinox))")
347 | print("June Solstice -> \(dateFormatter.string(from: juneSolstice))")
348 | print("September Equinox -> \(dateFormatter.string(from: septemberEquinox))")
349 | print("December Solstice -> \(dateFormatter.string(from: decemberSolstice))")
350 | }
351 |
352 | /*--------------------------------------------------------------------
353 | Private Variables
354 | *-------------------------------------------------------------------*/
355 |
356 | private var calendar: Calendar {
357 | var calendar: Calendar = .init(identifier: .gregorian)
358 | calendar.timeZone = self.timeZone
359 |
360 | return calendar
361 | }
362 |
363 | private var dateFormatter: DateFormatter {
364 | let dateFormatter = DateFormatter()
365 | dateFormatter.locale = .current
366 | dateFormatter.timeZone = self.timeZone
367 | dateFormatter.timeStyle = .full
368 | dateFormatter.dateStyle = .full
369 | return dateFormatter
370 | }
371 |
372 | private var timeZoneInSeconds: Int {
373 | timeZone.secondsFromGMT(for: self.date)
374 | }
375 |
376 | private var sunHorizonCoordinates: HorizonCoordinates = .init(altitude: .zero, azimuth: .zero)
377 |
378 |
379 | //Sun constants
380 | private let sunEclipticLongitudeAtTheEpoch: Angle = .init(degrees: 280.466069)
381 | private let sunEclipticLongitudePerigee: Angle = .init(degrees: 282.938346)
382 |
383 | /// Number of the days passed since the start of the year for the self.date
384 | private var daysPassedFromStartOfTheYear: Int {
385 | let year = calendar.component(.year, from: date)
386 |
387 | let dateFormatter: DateFormatter = DateFormatter()
388 |
389 | dateFormatter.dateFormat = "yyyy/mm/dd"
390 | dateFormatter.calendar = calendar
391 | let dataFormatted = dateFormatter.date(from: "\(year)/01/01")
392 |
393 | let startOfYear = calendar.startOfDay(for: dataFormatted!)
394 | let startOfDay = calendar.startOfDay(for: date)
395 |
396 | var daysPassedFromStartOfTheYear = calendar.dateComponents([.day], from: startOfYear, to: startOfDay).day!
397 | daysPassedFromStartOfTheYear = daysPassedFromStartOfTheYear + 1
398 |
399 | return daysPassedFromStartOfTheYear
400 | }
401 |
402 | private var b: Angle {
403 | let angleInDegrees: Double = (360 / 365 * Double(daysPassedFromStartOfTheYear - 81))
404 |
405 | return .degrees(angleInDegrees)
406 | }
407 |
408 | private var equationOfTime: Double {
409 | let bRad = b.radians
410 |
411 | return 9.87 * sin(2 * bRad) - 7.53 * cos(bRad) - 1.5 * sin(bRad)
412 | }
413 |
414 | private var localStandardTimeMeridian: Double {
415 | return (Double(self.timeZoneInSeconds) / SECONDS_IN_ONE_HOUR) * 15 //TimeZone in hour
416 | }
417 |
418 | private var timeCorrectionFactorInSeconds: Double {
419 | let timeCorrectionFactor = 4 * (location.coordinate.longitude - localStandardTimeMeridian) + equationOfTime
420 | let minutes: Double = Double(Int(timeCorrectionFactor) * 60)
421 | let seconds: Double = timeCorrectionFactor.truncatingRemainder(dividingBy: 1) * 100
422 | let timeCorrectionFactorInSeconds = minutes + seconds
423 |
424 | return timeCorrectionFactorInSeconds
425 | }
426 |
427 |
428 | /*--------------------------------------------------------------------
429 | Private methods
430 | *-------------------------------------------------------------------*/
431 |
432 | /// Updates in order all the sun coordinates: horizon, ecliptic and equatorial.
433 | /// Then get rise, set and noon times and their relative azimuths in degrees.
434 | /// Compute Solar noon.
435 | /// Compute Golden hour start and end time.
436 | /// Compute civil dusk and Civil Dawn time
437 | ///
438 | /// - Parameter needToComputeAgainSunEvents: True if Sunrise,Sunset and all the others daily sun events have to be computed.
439 | private mutating func refresh(needToComputeSunEvents: Bool = true) {
440 | updateSunCoordinates()
441 |
442 | if(needToComputeSunEvents){
443 | self.sunrise = getSunrise() ?? Date()
444 | self.sunriseAzimuth = getSunHorizonCoordinatesFrom(date: sunrise).azimuth.degrees
445 | self.sunset = getSunset() ?? Date()
446 | self.sunsetAzimuth = getSunHorizonCoordinatesFrom(date: sunset).azimuth.degrees
447 | self.solarNoon = getSolarNoon() ?? Date()
448 | self.solarMidnight = getSolarMidnight() ?? Date()
449 | self.solarNoonAzimuth = getSunHorizonCoordinatesFrom(date: solarNoon).azimuth.degrees
450 | self.eveningGoldenHourStart = getEveningGoldenHourStart() ?? Date()
451 | self.eveningGoldenHourEnd = getEveningGoldenHourEnd() ?? Date()
452 | self.civilDusk = getCivilDusk() ?? Date()
453 | self.civilDawn = getCivilDawn() ?? Date()
454 | self.nauticalDusk = getNauticalDusk() ?? Date()
455 | self.nauticalDawn = getNauticalDawn() ?? Date()
456 | self.astronomicalDusk = getAstronomicalDusk() ?? Date()
457 | self.astronomicalDawn = getAstronomicalDawn() ?? Date()
458 | self.morningGoldenHourStart = getMorningGoldenHourStart() ?? Date()
459 | self.morningGoldenHourEnd = getMorningGoldenHourEnd() ?? Date()
460 |
461 | }
462 |
463 | self.marchEquinox = getMarchEquinox() ?? Date()
464 | self.juneSolstice = getJuneSolstice() ?? Date()
465 | self.septemberEquinox = getSeptemberEquinox() ?? Date()
466 | self.decemberSolstice = getDecemberSolstice() ?? Date()
467 | }
468 |
469 | private func getSunMeanAnomaly(from elapsedDaysSinceStandardEpoch: Double) -> Angle {
470 | //Compute mean anomaly sun
471 | var sunMeanAnomaly: Angle = .init(degrees:(((360.0 * elapsedDaysSinceStandardEpoch) / 365.242191) + sunEclipticLongitudeAtTheEpoch.degrees - sunEclipticLongitudePerigee.degrees))
472 | sunMeanAnomaly = .init(degrees: extendedMod(sunMeanAnomaly.degrees, 360))
473 |
474 | return sunMeanAnomaly
475 | }
476 |
477 | private func getSunEclipticLongitude(from sunMeanAnomaly: Angle) -> Angle {
478 | //eclipticLatitude
479 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708
480 | let trueAnomaly = sunMeanAnomaly.degrees + equationOfCenter
481 | var eclipticLatitude: Angle = .init(degrees: trueAnomaly + sunEclipticLongitudePerigee.degrees)
482 |
483 | if eclipticLatitude.degrees > 360 {
484 | eclipticLatitude.degrees -= 360
485 | }
486 |
487 | return eclipticLatitude
488 | }
489 |
490 |
491 | /// Updates Horizon coordinates, Ecliptic coordinates and Equatorial coordinates of the Sun
492 | private mutating func updateSunCoordinates() {
493 | //Step1:
494 | //Convert LCT to UT, GST, and LST times and adjust the date if needed
495 | let gstHMS = uT2GST(self.date)
496 | let lstHMS = gST2LST(gstHMS,longitude: longitude)
497 |
498 | let lstDecimal = lstHMS.hMS2Decimal()
499 |
500 | //Step2:
501 | //Julian number for standard epoch 2000
502 | let jdEpoch = 2451545.00
503 |
504 | //Step3:
505 | //Compute the Julian day number for the desired date using the Greenwich date and TT
506 |
507 | let jdTT = jdFromDate(date: self.date)
508 |
509 | //Step5:
510 | //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe)
511 | let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De
512 |
513 | //Step6: Use the algorithm from section 6.2.3 to calculate the Sun’s ecliptic longitude and mean anomaly for the given UT date and time.
514 | let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch)
515 |
516 | //Step7: Use Equation 6.2.4 to aproximate the Equation of the center
517 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708
518 |
519 | //Step8: Add EoC to sun mean anomaly to get the sun true anomaly
520 | var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter
521 |
522 | //Step9:
523 | sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360)
524 |
525 | //Step10:
526 | var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees)
527 |
528 | //Step11:
529 | if sunEclipticLongitude.degrees > 360 {
530 | sunEclipticLongitude.degrees -= 360
531 | }
532 |
533 | sunEclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude)
534 |
535 | //Step12: Ecliptic to Equatorial
536 | sunEquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial()
537 |
538 | //Step13: Equatorial to Horizon
539 | sunHorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero)
540 |
541 | }
542 |
543 | public func getSunHorizonCoordinatesFrom(date: Date) -> HorizonCoordinates {
544 | //Step1:
545 | //Convert LCT to UT, GST, and LST times and adjust the date if needed
546 | let gstHMS = uT2GST(date)
547 | let lstHMS = gST2LST(gstHMS,longitude: longitude)
548 |
549 | let lstDecimal = lstHMS.hMS2Decimal()
550 |
551 | //Step2:
552 | //Julian number for standard epoch 2000
553 | let jdEpoch = 2451545.00
554 |
555 | //Step3:
556 | //Compute the Julian day number for the desired date using the Greenwich date and TT
557 |
558 | let jdTT = jdFromDate(date: date)
559 |
560 | //Step5:
561 | //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe)
562 | let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De
563 |
564 | //Step6: Use the algorithm from section 6.2.3 to calculate the Sun’s ecliptic longitude and mean anomaly for the given UT date and time.
565 | let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch)
566 |
567 | //Step7: Use Equation 6.2.4 to aproximate the Equation of the center
568 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708
569 |
570 | //Step8: Add EoC to sun mean anomaly to get the sun true anomaly
571 | var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter
572 |
573 | //Step9: Add or subtract multiples of 360° to adjust sun true anomaly to the range of 0° to 360°
574 | sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360)
575 |
576 |
577 | //Step10: Getting ecliptic longitude.
578 | var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees)
579 |
580 | //Step11:
581 | if sunEclipticLongitude.degrees > 360 {
582 | sunEclipticLongitude.degrees -= 360
583 | }
584 |
585 | let sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude)
586 |
587 | //Step12: Ecliptic to Equatorial
588 | var sunEquatorialCoordinates: EquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial()
589 |
590 | //Step13: Equatorial to Horizon
591 | let sunHorizonCoordinates: HorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero)
592 |
593 | return .init(altitude: sunHorizonCoordinates.altitude, azimuth: sunHorizonCoordinates.azimuth)
594 | }
595 |
596 | /// Computes the solar noon for self.date. Solar noon is the time when the sun is highest in the sky.
597 | /// - Returns: Solar noon time
598 | private func getSolarNoon() -> Date? {
599 | let secondsForUTCSolarNoon = (720 - 4 * location.coordinate.longitude - equationOfTime) * 60
600 | let secondsForSolarNoon = secondsForUTCSolarNoon + Double(timeZoneInSeconds)
601 | let startOfTheDay = calendar.startOfDay(for: date)
602 |
603 | let solarNoon = calendar.date(byAdding: .second, value: Int(secondsForSolarNoon) , to: startOfTheDay)
604 |
605 | return solarNoon
606 | }
607 |
608 | /// Computes the solar midnight for self.date.
609 | /// - Returns: Solar midnight time
610 | private func getSolarMidnight() -> Date? {
611 |
612 | let secondsForUTCSolarMidnight = (0 - 4 * location.coordinate.longitude - equationOfTime) * 60
613 | let secondsForSolarMidnight = secondsForUTCSolarMidnight + Double(timeZoneInSeconds)
614 | let startOfTheDay = calendar.startOfDay(for: date)
615 |
616 | let solarMidnight = calendar.date(byAdding: .second, value: Int(secondsForSolarMidnight) , to: startOfTheDay)
617 |
618 | return solarMidnight
619 | }
620 |
621 |
622 |
623 | /// Computes the Sunrise time for self.date
624 | /// - Returns: Sunrise time
625 | private func getSunrise() -> Date? {
626 | var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians)
627 |
628 | haArg = clamp(lower: -1, upper: 1, number: haArg)
629 | let ha: Angle = .radians(acos(haArg))
630 | let sunriseUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime
631 | var sunriseSeconds = (Int(sunriseUTCMinutes) * 60) + timeZoneInSeconds
632 | let startOfDay = calendar.startOfDay(for: date)
633 |
634 | if sunriseSeconds < Int(SECONDS_IN_ONE_HOUR) {
635 | sunriseSeconds = 0
636 | }
637 |
638 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunriseSeconds))
639 |
640 | let sunriseDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay)
641 |
642 | return sunriseDate
643 | }
644 |
645 | /// Computes the Sunset time for self.date
646 | /// - Returns: Sunset time
647 | private func getSunset() -> Date? {
648 | var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians)
649 |
650 | haArg = clamp(lower: -1, upper: 1, number: haArg)
651 | let ha: Angle = .radians(-acos(haArg))
652 | let sunsetUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime
653 | var sunsetSeconds = (Int(sunsetUTCMinutes) * 60) + timeZoneInSeconds
654 | let startOfDay = calendar.startOfDay(for: date)
655 |
656 | if sunsetSeconds > SECONDS_IN_ONE_DAY {
657 | sunsetSeconds = SECONDS_IN_ONE_DAY
658 | }
659 |
660 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunsetSeconds))
661 |
662 | let sunsetDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay)
663 |
664 | return sunsetDate
665 | }
666 |
667 | /// Computes the time at which the sun will reach the elevation given in input for self.date
668 | /// - Parameters:
669 | /// - elevation: Elevation
670 | /// - morning: Sun reaches a specific elevation twice, this boolean variable is needed to find out which one need to be considered. The one reached in the morning or not.
671 | /// - Returns: Time at which the Sun reaches that elevation. Nil if it didn't find it.
672 | private func getDateFrom(sunEvent : SunElevationEvents, morning: Bool = false) -> Date? {
673 |
674 | let elevationSun: Angle = .degrees(sunEvent.rawValue)
675 | var cosHra = (sin(elevationSun.radians) - sin(sunEquatorialCoordinates.declination.radians) * sin(latitude.radians)) / (cos(sunEquatorialCoordinates.declination.radians) * cos(latitude.radians))
676 | cosHra = clamp(lower: -1, upper: 1, number: cosHra)
677 | let hraAngle: Angle = .radians(acos(cosHra))
678 | var secondsForSunToReachElevation = (morning ? -1 : 1) * (hraAngle.degrees / 15) * SECONDS_IN_ONE_HOUR + TWELVE_HOUR_IN_SECONDS - timeCorrectionFactorInSeconds
679 | let startOfTheDay = calendar.startOfDay(for: date)
680 |
681 | if (Int(secondsForSunToReachElevation) > SECONDS_IN_ONE_DAY){
682 |
683 | secondsForSunToReachElevation = Double(SECONDS_IN_ONE_DAY)
684 | }
685 | else if (secondsForSunToReachElevation < SECONDS_IN_ONE_HOUR){
686 |
687 | secondsForSunToReachElevation = 0
688 | }
689 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(secondsForSunToReachElevation))
690 |
691 | let newDate = calendar.date(bySettingHour: hoursMinutesSeconds.0 , minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfTheDay)
692 |
693 |
694 | return newDate
695 | }
696 |
697 |
698 |
699 | /// Golden Hour in the evening begins when the sun reaches elevation equals to 6 degrees
700 | /// - Returns: Time at which the GoldenHour starts
701 | private func getEveningGoldenHourStart() -> Date? {
702 | guard let eveningGoldenHourStart = getDateFrom(sunEvent: .eveningGoldenHourStart) else {
703 | return nil
704 | }
705 |
706 | return eveningGoldenHourStart
707 | }
708 |
709 | /// Golden Hour in the evening ends when the sun reaches elevation equals to -4 degrees
710 | /// - Returns: Time at which the GoldenHour ends
711 | private func getEveningGoldenHourEnd() -> Date? {
712 | guard let goldenHourFinish = getDateFrom(sunEvent: .eveningGoldenHourEnd) else {
713 | return nil
714 | }
715 |
716 | return goldenHourFinish
717 | }
718 |
719 | /// Civil Dawn is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise
720 | /// - Returns: Civil Dawn time
721 | private func getCivilDawn() -> Date? {
722 | guard let civilDawn = getDateFrom(sunEvent: .civil,morning: true) else {
723 | return nil
724 | }
725 | return civilDawn
726 | }
727 |
728 | /// civil dusk is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise.
729 | /// - Returns: civil dusk time
730 | private func getCivilDusk() -> Date? {
731 | guard let civilDusk = getDateFrom(sunEvent: .civil, morning: false) else {
732 | return nil
733 | }
734 | return civilDusk
735 | }
736 |
737 | /// Nautical Dusk is when the Sun reaches -12 degrees of elevation.
738 | /// - Returns: Nautical Dusk
739 | private func getNauticalDusk() -> Date? {
740 | guard let nauticalDusk = getDateFrom(sunEvent: .nautical, morning: false) else {
741 | return nil
742 | }
743 | return nauticalDusk
744 | }
745 |
746 | /// Nautical Dusk is when the Sun reaches -12 degrees of elevation.
747 | /// - Returns: Nautical Dawn
748 | private func getNauticalDawn() -> Date? {
749 | guard let nauticalDawn = getDateFrom(sunEvent: .nautical, morning: true) else {
750 | return nil
751 | }
752 | return nauticalDawn
753 | }
754 |
755 | /// Astronomical Dusk is when the Sun reaches -18 degrees of elevation.
756 | /// - Returns: Astronomical Dusk
757 | private func getAstronomicalDusk() -> Date? {
758 | guard let astronomicalDusk = getDateFrom(sunEvent: .astronomical, morning: false) else {
759 | return nil
760 | }
761 | return astronomicalDusk
762 | }
763 |
764 | /// Astronomical Dawn is when the Sun reaches -18 degrees of elevation.
765 | /// - Returns: Astronomical Dawn
766 | private func getAstronomicalDawn() -> Date? {
767 | guard let astronomicalDawn = getDateFrom(sunEvent: .astronomical, morning: true) else {
768 | return nil
769 | }
770 | return astronomicalDawn
771 | }
772 |
773 | /// Morning Golden Hour start when Sun reaches -4 degress of elevation
774 | /// - Returns: Morning golden hour start
775 | private func getMorningGoldenHourStart() -> Date? {
776 | guard let morningGoldenHourStart = getDateFrom(sunEvent: .morningGoldenHourStart , morning: true) else {
777 | return nil
778 | }
779 | return morningGoldenHourStart
780 | }
781 |
782 | /// Morning Golden Hour ends when Sun reaches 6 degress of elevation
783 | /// - Returns: Morning golden hour end
784 | private func getMorningGoldenHourEnd() -> Date? {
785 | guard let morningGoldenHourEnd = getDateFrom(sunEvent: .morningGoldenHourEnd , morning: true) else {
786 | return nil
787 | }
788 | return morningGoldenHourEnd
789 | }
790 |
791 | private func getMarchEquinox() -> Date? {
792 |
793 | let year = Double(calendar.component(.year, from: self.date))
794 | let t: Double = year / 1000
795 | let julianDayMarchEquinox: Double = 1721139.2855 + 365.2421376 * year + 0.0679190 * pow(t, 2) - 0.0027879 * pow(t, 3)
796 |
797 | let marchEquinoxUTC = dateFromJd(jd: julianDayMarchEquinox)
798 |
799 | return marchEquinoxUTC
800 | }
801 |
802 | private func getJuneSolstice() -> Date? {
803 |
804 | let year = Double(calendar.component(.year, from: self.date))
805 | let t: Double = year / 1000
806 | let julianDayJuneSolstice: Double = 1721233.2486 + 365.2417284 * year - 0.0530180 * pow(t, 2) + 0.0093320 * pow(t, 3)
807 |
808 | let juneSolsticeUTC = dateFromJd(jd: julianDayJuneSolstice)
809 |
810 | return juneSolsticeUTC
811 | }
812 |
813 | private func getSeptemberEquinox() -> Date? {
814 |
815 | let year = Double(calendar.component(.year, from: self.date))
816 | let t: Double = year / 1000
817 | let julianDaySeptemberEquinox: Double = 1721325.6978 + 365.2425055 * year - 0.126689 * pow(t, 2) + 0.0019401 * pow(t, 3)
818 |
819 | let septemberEquinoxUTC = dateFromJd(jd: julianDaySeptemberEquinox)
820 |
821 | return septemberEquinoxUTC
822 | }
823 |
824 | private func getDecemberSolstice() -> Date? {
825 |
826 | let year = Double(calendar.component(.year, from: self.date))
827 | let t: Double = year / 1000
828 | let julianDayDecemberSolstice: Double = 1721414.3920 + 365.2428898 * year - 0.0109650 * pow(t, 2) - 0.0084885 * pow(t, 3)
829 |
830 | let decemberSolsticeUTC = dateFromJd(jd: julianDayDecemberSolstice)
831 |
832 | return decemberSolsticeUTC
833 | }
834 |
835 |
836 | /// - Returns: Length in meters of the object's shadow by the provided object height and current sun altitude.
837 | public func shadowLength(for objectHeight: Double = 1, with altitude: Angle? = nil) -> Double? {
838 | let altitude = altitude ?? self.altitude
839 |
840 | return if altitude.degrees > 0 && altitude.degrees < 90 {
841 | objectHeight / tan(altitude.radians)
842 | } else if altitude.degrees <= 0 {
843 | nil
844 | } else {
845 | 0
846 | }
847 | }
848 | }
849 |
850 | extension Sun: Equatable {
851 | public static func == (lhs: Sun, rhs: Sun) -> Bool {
852 | lhs.location == rhs.location &&
853 | lhs.timeZone == rhs.timeZone &&
854 | lhs.date == rhs.date
855 | }
856 | }
857 |
858 | extension Sun: Hashable {
859 | public func hash(into hasher: inout Hasher) {
860 | hasher.combine(location)
861 | hasher.combine(timeZone)
862 | hasher.combine(date)
863 | }
864 | }
865 |
--------------------------------------------------------------------------------