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  }