github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/common/astronomics/suncalc/suncalc.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package suncalc 20 21 // Translated in GO from the NPM library: 22 // https://github.com/mourner/suncalc 23 24 import ( 25 "math" 26 "time" 27 ) 28 29 // date/DayTime constants and conversions 30 const millyToNano = 1000000 31 const dayMs = 1000 * 60 * 60 * 24 32 33 // J1970 ... 34 const J1970 = 2440588 35 36 // J2000 ... 37 const J2000 = 2451545 38 39 func timeToUnixMillis(date time.Time) int64 { return int64(float64(date.UnixNano()) / millyToNano) } 40 func unixMillisToTime(date float64) time.Time { return time.Unix(0, int64(date*millyToNano)) } 41 func toJulian(date time.Time) float64 { return float64(timeToUnixMillis(date))/dayMs - 0.5 + J1970 } 42 func fromJulian(j float64) time.Time { return unixMillisToTime((j + 0.5 - J1970) * dayMs) } 43 func toDays(date time.Time) float64 { return toJulian(date) - J2000 } 44 45 // general calculations for position 46 const rad = math.Pi / 180 47 const e = rad * 23.4397 // obliquity of the Earth 48 49 func rightAscension(l float64, b float64) float64 { 50 return math.Atan2(math.Sin(l)*math.Cos(e)-math.Tan(b)*math.Sin(e), math.Cos(l)) 51 } 52 53 func declination(l float64, b float64) float64 { 54 return math.Asin(math.Sin(b)*math.Cos(e) + math.Cos(b)*math.Sin(e)*math.Sin(l)) 55 } 56 57 func azimuth(H float64, phi float64, dec float64) float64 { 58 return math.Atan2(math.Sin(H), math.Cos(H)*math.Sin(phi)-math.Tan(dec)*math.Cos(phi)) 59 } 60 61 func altitude(H float64, phi float64, dec float64) float64 { 62 return math.Asin(math.Sin(phi)*math.Sin(dec) + math.Cos(phi)*math.Cos(dec)*math.Cos(H)) 63 } 64 65 func siderealTime(d float64, lw float64) float64 { return rad*(280.16+360.9856235*d) - lw } 66 67 func astroRefraction(h float64) float64 { 68 if h < 0.0 { 69 h = 0 // if h = -0.08901179 a div/0 would occur. 70 } // the following formula works for positive altitudes only. 71 72 // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. 73 // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: 74 return 0.0002967 / math.Tan(h+0.00312536/(h+0.08901179)) 75 } 76 77 // general sun calculations 78 79 func solarMeanAnomalyI(d float64) float64 { return solarMeanAnomalyF(d) } 80 func solarMeanAnomalyF(d float64) float64 { return rad * (357.5291 + 0.98560028*d) } 81 82 func eclipticLongitude(M float64) float64 { 83 84 var C = rad * (1.9148*math.Sin(M) + 0.02*math.Sin(2*M) + 0.0003*math.Sin(3*M)) // equation of center 85 var P = rad * 102.9372 // perihelion of the Earth 86 87 return M + C + P + math.Pi 88 } 89 90 // DayTimeName ... 91 type DayTimeName string 92 93 const ( 94 // Sunrise ... 95 Sunrise DayTimeName = "sunrise" 96 // Sunset ... 97 Sunset DayTimeName = "sunset" 98 99 // SunriseEnd ... 100 SunriseEnd DayTimeName = "sunriseEnd" 101 // SunsetStart ... 102 SunsetStart DayTimeName = "sunsetStart" 103 104 // Dawn ... 105 Dawn DayTimeName = "dawn" 106 // Dusk ... 107 Dusk DayTimeName = "dusk" 108 109 // NauticalDawn ... 110 NauticalDawn DayTimeName = "nauticalDawn" 111 // NauticalDusk ... 112 NauticalDusk DayTimeName = "nauticalDusk" 113 114 // NightEnd ... 115 NightEnd DayTimeName = "nightEnd" 116 // Night ... 117 Night DayTimeName = "night" 118 119 // GoldenHourEnd ... 120 GoldenHourEnd DayTimeName = "goldenHourEnd" 121 // GoldenHour ... 122 GoldenHour DayTimeName = "goldenHour" 123 124 // SolarNoon ... 125 SolarNoon DayTimeName = "solarNoon" 126 // Nadir ... 127 Nadir DayTimeName = "nadir" 128 ) 129 130 // DayTime ... 131 type DayTime struct { 132 MorningName DayTimeName 133 Time time.Time 134 } 135 136 type dayTimeConf struct { 137 angle float64 138 morningName DayTimeName 139 eveningName DayTimeName 140 } 141 142 type coord struct { 143 dec float64 144 ra float64 145 } 146 147 func sunCoords(d float64) coord { 148 var M = solarMeanAnomalyI(d) 149 var L = eclipticLongitude(M) 150 151 return coord{ 152 declination(L, 0), 153 rightAscension(L, 0), 154 } 155 } 156 157 // SunPosition ... 158 type SunPosition struct { 159 Azimuth float64 160 Altitude float64 161 } 162 163 // calculates sun position for a given date and latitude/longitude 164 func GetSunPosition(date time.Time, lat float64, lng float64) SunPosition { 165 166 var lw = rad * -lng 167 var phi = rad * lat 168 var d = toDays(date) 169 var c = sunCoords(d) 170 var H = siderealTime(d, lw) - c.ra 171 172 return SunPosition{ 173 azimuth(H, phi, c.dec), 174 altitude(H, phi, c.dec), 175 } 176 } 177 178 // IsNight ... 179 func IsNight(sunrisePos SunPosition) bool { 180 solarElevation := sunrisePos.Altitude * 180 / math.Pi 181 return solarElevation < -0.833 182 } 183 184 // sun times configuration (angle, morning name, evening name) 185 var times = []dayTimeConf{ 186 {-0.833, Sunrise, Sunset}, 187 {-0.3, SunriseEnd, SunsetStart}, 188 {-6, Dawn, Dusk}, 189 {-12, NauticalDawn, NauticalDusk}, 190 {-18, NightEnd, Night}, 191 {6, GoldenHourEnd, GoldenHour}, 192 } 193 194 // calculations for sun times 195 const J0 = 0.0009 196 197 func julianCycle(d float64, lw float64) float64 { return math.Round(d - J0 - lw/(2*math.Pi)) } 198 199 func approxTransit(Ht float64, lw float64, n float64) float64 { 200 return J0 + (Ht+lw)/(2*math.Pi) + n 201 } 202 func solarTransitJ(ds float64, M float64, L float64) float64 { 203 return J2000 + ds + 0.0053*math.Sin(M) - 0.0069*math.Sin(2*L) 204 } 205 func hourAngle(h float64, phi float64, d float64) float64 { 206 return math.Acos((math.Sin(h) - math.Sin(phi)*math.Sin(d)) / (math.Cos(phi) * math.Cos(d))) 207 } 208 209 // returns set DayTime for the given sun altitude 210 func getSetJ(h float64, lw float64, phi float64, dec float64, n float64, M float64, L float64) float64 { 211 var w = hourAngle(h, phi, dec) 212 var a = approxTransit(w, lw, n) 213 return solarTransitJ(a, M, L) 214 } 215 216 // calculates sun times for a given date and latitude/longitude 217 func GetTimes(date time.Time, lat float64, lng float64) map[DayTimeName]DayTime { 218 lw := rad * -lng 219 phi := rad * lat 220 221 d := toDays(date) 222 n := julianCycle(d, lw) 223 ds := approxTransit(0, lw, n) 224 225 M := solarMeanAnomalyF(ds) 226 L := eclipticLongitude(M) 227 dec := declination(L, 0) 228 229 Jnoon := solarTransitJ(ds, M, L) 230 231 //i, len, DayTime, Jset, Jrise; 232 var oneTime dayTimeConf 233 result := make(map[DayTimeName]DayTime) 234 235 result[SolarNoon] = DayTime{SolarNoon, fromJulian(Jnoon)} 236 result[Nadir] = DayTime{Nadir, fromJulian(Jnoon - 0.5)} 237 238 for i := 0; i < len(times); i++ { 239 oneTime = times[i] 240 241 Jset := getSetJ(oneTime.angle*rad, lw, phi, dec, n, M, L) 242 Jrise := Jnoon - (Jset - Jnoon) 243 244 result[oneTime.morningName] = DayTime{oneTime.morningName, fromJulian(Jrise)} 245 result[oneTime.eveningName] = DayTime{oneTime.eveningName, fromJulian(Jset)} 246 } 247 248 return result 249 } 250 251 type moonCoordinates struct { 252 rightAscension float64 253 declination float64 254 distance float64 255 } 256 257 // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas 258 func moonCoords(d float64) moonCoordinates { // geocentric ecliptic coordinates of the moon 259 L := rad * (218.316 + 13.176396*d) // ecliptic longitude 260 M := rad * (134.963 + 13.064993*d) // mean anomaly 261 F := rad * (93.272 + 13.229350*d) // mean distance 262 263 l := L + rad*6.289*math.Sin(M) // longitude 264 b := rad * 5.128 * math.Sin(F) // latitude 265 dt := 385001 - 20905*math.Cos(M) // distance to the moon in km 266 267 return moonCoordinates{ 268 rightAscension(l, b), 269 declination(l, b), 270 dt, 271 } 272 } 273 274 // MoonPosition ... 275 type MoonPosition struct { 276 Azimuth float64 277 Altitude float64 278 Distance float64 279 ParallacticAngle float64 280 } 281 282 // GetMoonPosition ... 283 func GetMoonPosition(date time.Time, lat float64, lng float64) MoonPosition { 284 lw := rad * -lng 285 phi := rad * lat 286 d := toDays(date) 287 288 c := moonCoords(d) 289 H := siderealTime(d, lw) - c.rightAscension 290 h := altitude(H, phi, c.declination) 291 // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. 292 pa := math.Atan2(math.Sin(H), math.Tan(phi)*math.Cos(c.declination)-math.Sin(c.declination)*math.Cos(H)) 293 h = h + astroRefraction(h) // altitude correction for refraction 294 295 return MoonPosition{ 296 azimuth(H, phi, c.declination), 297 h, 298 c.distance, 299 pa, 300 } 301 } 302 303 // MoonIllumination ... 304 type MoonIllumination struct { 305 fraction float64 306 phase float64 307 angle float64 308 } 309 310 // calculations for illumination parameters of the moon, 311 // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and 312 // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. 313 func GetMoonIllumination(date time.Time) MoonIllumination { 314 315 d := toDays(date) 316 s := sunCoords(d) 317 m := moonCoords(d) 318 319 sdist := 149598000. // distance from Earth to Sun in km 320 321 phi := math.Acos(math.Sin(s.dec)*math.Sin(m.declination) + math.Cos(s.dec)*math.Cos(m.declination)*math.Cos(s.ra-m.rightAscension)) 322 inc := math.Atan2(sdist*math.Sin(phi), m.distance-sdist*math.Cos(phi)) 323 angle := math.Atan2(math.Cos(s.dec)*math.Sin(s.ra-m.rightAscension), math.Sin(s.dec)*math.Cos(m.declination)-math.Cos(s.dec)*math.Sin(m.declination)*math.Cos(s.ra-m.rightAscension)) 324 phaseAngle := 1. 325 if angle < 0 { 326 phaseAngle = -1. 327 } 328 329 return MoonIllumination{ 330 (1 + math.Cos(inc)) / 2, 331 0.5 + 0.5*inc*phaseAngle/math.Pi, 332 angle, 333 } 334 } 335 336 func hoursLater(date time.Time, h int64) time.Time { 337 return date.Add(time.Duration(h * dayMs / 24 * millyToNano)) 338 } 339 340 // MoonTimes ... 341 type MoonTimes struct { 342 Rise time.Time 343 Set time.Time 344 AlwaysUp bool 345 AlwaysDown bool 346 } 347 348 // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article 349 func GetMoonTimes(date time.Time, lat float64, lng float64, inUTC bool) MoonTimes { 350 var t time.Time 351 if inUTC { 352 t = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) 353 } else { 354 t = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) 355 } 356 357 hc := 0.133 * rad 358 h0 := GetMoonPosition(t, lat, lng).Altitude - hc 359 //h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; 360 var ye float64 361 var x1 float64 362 var x2 float64 363 var rise float64 364 var set float64 365 366 // go in 2-hour chunks, each DayTime seeing if a 3-point quadratic curve crosses zero (which means rise or set) 367 i := int64(0) 368 for i <= 24 { 369 370 h1 := GetMoonPosition(hoursLater(t, i), lat, lng).Altitude - hc 371 h2 := GetMoonPosition(hoursLater(t, i+1), lat, lng).Altitude - hc 372 a := (h0+h2)/2 - h1 373 b := (h2 - h0) / 2 374 xe := -b / (2 * a) 375 ye = (a*xe+b)*xe + h1 376 d := b*b - 4*a*h1 377 roots := 0 378 if d >= 0 { 379 dx := math.Sqrt(d) / (math.Abs(a) * 2) 380 x1 = xe - dx 381 x2 = xe + dx 382 if math.Abs(x1) <= 1 { 383 roots++ 384 } 385 if math.Abs(x2) <= 1 { 386 roots++ 387 } 388 if x1 < -1 { 389 x1 = x2 390 } 391 } 392 393 if roots == 1 { 394 if h0 < 0 { 395 rise = float64(i) + x1 396 } else { 397 set = float64(i) + x1 398 } 399 400 } else { 401 if roots == 2 { 402 if ye < 0 { 403 rise = float64(i) + x2 404 set = float64(i) + x1 405 } else { 406 rise = float64(i) + x1 407 set = float64(i) + x2 408 } 409 } 410 } 411 if rise != 0 && set != 0 { 412 break 413 } 414 415 h0 = h2 416 i += 2 417 } 418 419 var result = MoonTimes{} 420 421 if rise != 0 { 422 result.Rise = hoursLater(t, int64(rise)) 423 } 424 if set != 0 { 425 result.Set = hoursLater(t, int64(set)) 426 } 427 if rise == 0 && set == 0 { 428 if ye > 0 { 429 result.AlwaysUp = true 430 } else { 431 result.AlwaysDown = true 432 } 433 } 434 435 return result 436 }