github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/common/astronomics/moonphase/moonphase.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 moonphase
    20  
    21  import (
    22  	"math"
    23  	"time"
    24  )
    25  
    26  // Moon ...
    27  type Moon struct {
    28  	phase     float64
    29  	illum     float64
    30  	age       float64
    31  	dist      float64
    32  	angdia    float64
    33  	sundist   float64
    34  	sunangdia float64
    35  	pdata     float64
    36  	quarters  [8]float64
    37  	timespace float64
    38  }
    39  
    40  var synmonth float64 = 29.53058868 // Synodic month (new Moon to new Moon)
    41  
    42  // New ...
    43  func New(t time.Time) (moonP *Moon) {
    44  	moonP = new(Moon)
    45  
    46  	// Astronomical constants
    47  	var epoch float64 = 2444238.5 // 1989 January 0.0
    48  
    49  	//Constants defining the Sun's apparent orbit
    50  	var elonge float64 = 278.833540  // Ecliptic longitude of the Sun at epoch 1980.0
    51  	var elongp float64 = 282.596403  // Ecliptic longitude of the Sun at perigee
    52  	var eccent float64 = 0.016718    // Eccentricity of Earth's orbit
    53  	var sunsmax float64 = 1.495985e8 // Sun's angular size, degrees, at semi-major axis distance
    54  	var sunangsiz float64 = 0.533128
    55  
    56  	// Elements of the Moon's orbit, epoch 1980.0
    57  	var mmlong float64 = 64.975464   // Moon's mean longitude at the epoch
    58  	var mmlongp float64 = 349.383063 // Mean longitude of the perigee at the epoch
    59  	var mecc float64 = 0.054900      // Eccentricity of the Moon's orbit
    60  	var mangsiz float64 = 0.5181     // Moon's angular size at distance a from Earth
    61  	var msmax float64 = 384401       // Semi-major axis of Moon's orbit in km
    62  
    63  	moonP.timespace = float64(t.Unix())
    64  	moonP.pdata = utcToJulian(float64(t.Unix()))
    65  	// Calculation of the Sun's position
    66  	var day = moonP.pdata - epoch // Date within epoch
    67  
    68  	var n float64 = fixangle((360 / 365.2422) * day) // Mean anomaly of the Sun
    69  	var m float64 = fixangle(n + elonge - elongp)    // Convert from perigee co-orginates to epoch 1980.0
    70  	var ec = kepler(m, eccent)                       // Solve equation of Kepler
    71  	ec = math.Sqrt((1+eccent)/(1-eccent)) * math.Tan(ec/2)
    72  	ec = 2 * rad2deg(math.Atan(ec))               // True anomaly
    73  	var lambdasun float64 = fixangle(ec + elongp) // Sun's geocentric ecliptic longitude
    74  
    75  	var f float64 = ((1 + eccent*cos(deg2rad(ec))) / (1 - eccent*eccent)) // Orbital distance factor
    76  	var sunDist float64 = sunsmax / f                                     // Distance to Sun in km
    77  	var sunAng float64 = f * sunangsiz                                    // Sun's angular size in degrees
    78  
    79  	// Calsulation of the Moon's position
    80  	var ml float64 = fixangle(13.1763966*day + mmlong)          // Moon's mean longitude
    81  	var mm float64 = fixangle(ml - 0.1114041*day - mmlongp)     // Moon's mean anomaly
    82  	var ev float64 = 1.2739 * sin(deg2rad(2*(ml-lambdasun)-mm)) // Evection
    83  	var ae float64 = 0.1858 * sin(deg2rad(m))                   // Annual equation
    84  	var a3 float64 = 0.37 * sin(deg2rad(m))                     // Correction term
    85  	var mmP float64 = mm + ev - ae - a3                         // Corrected anomaly
    86  	var mec float64 = 6.2886 * sin(deg2rad(mmP))                // Correction for the equation of the centre
    87  	var a4 float64 = 0.214 * sin(deg2rad(2*mmP))                // Another correction term
    88  	var lP float64 = ml + ev + mec - ae + a4                    // Corrected longitude
    89  	var v float64 = 0.6583 * sin(deg2rad(2*(lP-lambdasun)))     // Variation
    90  	var lPP float64 = lP + v                                    // True longitude
    91  
    92  	// Calculation of the phase of the Moon
    93  	var moonAge float64 = lPP - lambdasun                   // Age of the Moon in degrees
    94  	var moonPhase float64 = (1 - cos(deg2rad(moonAge))) / 2 // Phase of the Moon
    95  
    96  	// Distance of moon from the centre of the Earth
    97  	var moonDist float64 = (msmax * (1 - mecc*mecc)) / (1 + mecc*cos(deg2rad(mmP+mec)))
    98  
    99  	var moonDFrac float64 = moonDist / msmax
   100  	var moonAng float64 = mangsiz / moonDFrac // Moon's angular diameter
   101  
   102  	// store result
   103  	moonP.phase = fixangle(moonAge) / 360 // Phase (0 to 1)
   104  	moonP.illum = moonPhase               // Illuminated fraction (0 to 1)
   105  	moonP.age = synmonth * moonP.phase    // Age of moon (days)
   106  	moonP.dist = moonDist                 // Distance (kilometres)
   107  	moonP.angdia = moonAng                // Angular diameter (degreees)
   108  	moonP.sundist = sunDist               // Distance to Sun (kilometres)
   109  	moonP.sunangdia = sunAng              // Sun's angular diameter (degrees)
   110  	moonP.phaseHunt()
   111  	return moonP
   112  }
   113  
   114  func sin(a float64) float64 {
   115  	return math.Sin(a)
   116  }
   117  
   118  func cos(a float64) float64 {
   119  	return math.Cos(a)
   120  }
   121  
   122  func rad2deg(r float64) float64 {
   123  	return (r * 180) / math.Pi
   124  }
   125  
   126  func deg2rad(d float64) float64 {
   127  	return (d * math.Pi) / 180
   128  }
   129  
   130  func fixangle(a float64) float64 {
   131  	return (a - 360*math.Floor(a/360))
   132  }
   133  
   134  func kepler(m float64, ecc float64) float64 {
   135  	epsilon := 0.000001
   136  	m = deg2rad(m)
   137  	e := m
   138  	var delta float64
   139  	delta = e - ecc*math.Sin(e) - m
   140  	e -= delta / (1 - ecc*math.Cos(e))
   141  	for math.Abs(delta) > epsilon {
   142  		delta = e - ecc*math.Sin(e) - m
   143  		e -= delta / (1 - ecc*math.Cos(e))
   144  	}
   145  	return e
   146  }
   147  
   148  func (m *Moon) phaseHunt() {
   149  	var sdate = utcToJulian(m.timespace)
   150  	var adate = sdate - 45
   151  	var ats = m.timespace - 86400*45
   152  	t := time.Unix(int64(ats), 0)
   153  	var yy = float64(t.Year())
   154  	var mm = float64(t.Month())
   155  
   156  	//var k1 = math.Floor(float64(yy+((mm-1)*(1/12))-1900) * 12.3685)
   157  	var k1 = math.Floor(float64(yy+((mm-1)*(1.0/12.0))-1900) * 12.3685)
   158  	var nt1 = meanPhase(adate, k1)
   159  	adate = nt1
   160  	var nt2, k2 float64
   161  
   162  	for {
   163  		adate += synmonth
   164  		k2 = k1 + 1
   165  		nt2 = meanPhase(adate, k2)
   166  		if math.Abs(nt2-sdate) < 0.75 {
   167  			nt2 = truePhase(k2, 0.0)
   168  		}
   169  		if nt1 <= sdate && nt2 > sdate {
   170  			break
   171  		}
   172  		nt1 = nt2
   173  		k1 = k2
   174  	}
   175  
   176  	var data [8]float64
   177  
   178  	data[0] = truePhase(k1, 0.0)
   179  	data[1] = truePhase(k1, 0.25)
   180  	data[2] = truePhase(k1, 0.5)
   181  	data[3] = truePhase(k1, 0.75)
   182  	data[4] = truePhase(k2, 0.0)
   183  	data[5] = truePhase(k2, 0.25)
   184  	data[6] = truePhase(k2, 0.5)
   185  	data[7] = truePhase(k2, 0.75)
   186  
   187  	for i := 0; i < 8; i++ {
   188  		m.quarters[i] = (data[i] - 2440587.5) * 86400 // convert to UNIX time
   189  	}
   190  }
   191  
   192  func utcToJulian(t float64) float64 {
   193  	return t/86400 + 2440587.5
   194  }
   195  
   196  //func julianToUtc(t float64) float64 {
   197  //	return t*86400 + 2440587.5
   198  //}
   199  
   200  /*
   201  *
   202  
   203  	Calculates time of the mean new Moon for a given
   204  	base date. This argument K to this function is the
   205  	precomputed synodic month index, given by:
   206  	    K = (year - 1900) * 12.3685
   207  	where year is expressed as a year aand fractional year
   208  */
   209  func meanPhase(sdate float64, k float64) float64 {
   210  	// Time in Julian centuries from 1900 January 0.5
   211  	var t float64 = (sdate - 2415020.0) / 36525
   212  	var t2 float64 = t * t
   213  	var t3 float64 = t2 * t
   214  
   215  	nt := 2415020.75933 + synmonth*k +
   216  		0.0001178*t2 -
   217  		0.000000155*t3 +
   218  		0.00033*sin(deg2rad(166.56+132.87*t-0.009173*t2))
   219  
   220  	return nt
   221  }
   222  
   223  func truePhase(k float64, phase float64) float64 {
   224  	k += phase                  // AddEntity phase to new moon time
   225  	var t float64 = k / 1236.85 // Time in Julian centures from 1900 January 0.5
   226  	var t2 float64 = t * t
   227  	var t3 float64 = t2 * t
   228  	var pt float64
   229  	pt = 2415020.75933 + synmonth*k +
   230  		0.0001178*t2 -
   231  		0.000000155*t3 +
   232  		0.00033*sin(deg2rad(166.56+132.87*t-0.009173*t2))
   233  
   234  	var m, mprime, f float64
   235  	m = 359.2242 + 29.10535608*k - 0.0000333*t2 - 0.00000347*t3       // Sun's mean anomaly
   236  	mprime = 306.0253 + 385.81691806*k + 0.0107306*t2 + 0.00001236*t3 // Moon's mean anomaly
   237  	f = 21.2964 + 390.67050646*k - 0.0016528*t2 - 0.00000239*t3       // Moon's argument of latitude
   238  
   239  	if phase < 0.01 || math.Abs(phase-0.5) < 0.01 {
   240  		// Corrections for New and Full Moon
   241  		pt += (0.1734-0.000393*t)*sin(deg2rad(m)) +
   242  			0.0021*sin(deg2rad(2*m)) -
   243  			0.4068*sin(deg2rad(mprime)) +
   244  			0.0161*sin(deg2rad(2*mprime)) -
   245  			0.0004*sin(deg2rad(3*mprime)) +
   246  			0.0104*sin(deg2rad(2*f)) -
   247  			0.0051*sin(deg2rad(m+mprime)) -
   248  			0.0074*sin(deg2rad(m-mprime)) +
   249  			0.0004*sin(deg2rad(2*f+m)) -
   250  			0.0004*sin(deg2rad(2*f-m)) -
   251  			0.0006*sin(deg2rad(2*f+mprime)) +
   252  			0.0010*sin(deg2rad(2*f-mprime)) +
   253  			0.0005*sin(deg2rad(m+2*mprime))
   254  	} else if math.Abs(phase-0.25) < 0.01 || math.Abs(phase-0.75) < 0.01 {
   255  		pt += (0.1721-0.0004*t)*sin(deg2rad(m)) +
   256  			0.0021*sin(deg2rad(2*m)) -
   257  			0.6280*sin(deg2rad(mprime)) +
   258  			0.0089*sin(deg2rad(2*mprime)) -
   259  			0.0004*sin(deg2rad(3*mprime)) +
   260  			0.0079*sin(deg2rad(2*f)) -
   261  			0.0119*sin(deg2rad(m+mprime)) -
   262  			0.0047*sin(deg2rad(m-mprime)) +
   263  			0.0003*sin(deg2rad(2*f+m)) -
   264  			0.0004*sin(deg2rad(2*f-m)) -
   265  			0.0006*sin(deg2rad(2*f+mprime)) +
   266  			0.0021*sin(deg2rad(2*f-mprime)) +
   267  			0.0003*sin(deg2rad(m+2*mprime)) +
   268  			0.0004*sin(deg2rad(m-2*mprime)) -
   269  			0.0003*sin(deg2rad(2*m+mprime))
   270  		if phase < 0.5 { // First quarter correction
   271  			pt += 0.0028 - 0.0004*cos(deg2rad(m)) + 0.0003*cos(deg2rad(mprime))
   272  		} else { // Last quarter correction
   273  			pt += -0.0028 + 0.0004*cos(deg2rad(m)) - 0.0003*cos(deg2rad(mprime))
   274  		}
   275  	}
   276  
   277  	return pt
   278  }
   279  
   280  // Phase ...
   281  func (m *Moon) Phase() float64 {
   282  	return m.phase
   283  }
   284  
   285  // Illumination ...
   286  func (m *Moon) Illumination() float64 {
   287  	return m.illum
   288  }
   289  
   290  // Age ...
   291  func (m *Moon) Age() float64 {
   292  	return m.age
   293  }
   294  
   295  // Distance ...
   296  func (m *Moon) Distance() float64 {
   297  	return m.dist
   298  }
   299  
   300  // Diameter ...
   301  func (m *Moon) Diameter() float64 {
   302  	return m.angdia
   303  }
   304  
   305  // SunDistance ...
   306  func (m *Moon) SunDistance() float64 {
   307  	return m.sundist
   308  }
   309  
   310  // SunDiameter ...
   311  func (m *Moon) SunDiameter() float64 {
   312  	return m.sunangdia
   313  }
   314  
   315  // NewMoon ...
   316  func (m *Moon) NewMoon() float64 {
   317  	return m.quarters[0]
   318  }
   319  
   320  // FirstQuarter ...
   321  func (m *Moon) FirstQuarter() float64 {
   322  	return m.quarters[1]
   323  }
   324  
   325  // FullMoon ...
   326  func (m *Moon) FullMoon() float64 {
   327  	return m.quarters[2]
   328  }
   329  
   330  // LastQuarter ...
   331  func (m *Moon) LastQuarter() float64 {
   332  	return m.quarters[3]
   333  }
   334  
   335  // NextNewMoon ...
   336  func (m *Moon) NextNewMoon() float64 {
   337  	return m.quarters[4]
   338  }
   339  
   340  // NextFirstQuarter ...
   341  func (m *Moon) NextFirstQuarter() float64 {
   342  	return m.quarters[1]
   343  }
   344  
   345  // NextFullMoon ...
   346  func (m *Moon) NextFullMoon() float64 {
   347  	return m.quarters[6]
   348  }
   349  
   350  // NextLastQuarter ...
   351  func (m *Moon) NextLastQuarter() float64 {
   352  	return m.quarters[7]
   353  }
   354  
   355  // PhaseName ...
   356  func (m *Moon) PhaseName() string {
   357  	names := map[int]string{
   358  		0: "New Moon",
   359  		1: "Waxing Crescent",
   360  		2: "First Quarter",
   361  		3: "Waxing Gibbous",
   362  		4: "Full Moon",
   363  		5: "Waning Gibbous",
   364  		6: "Third Quarter",
   365  		7: "Waning Crescent",
   366  		8: "New Moon",
   367  	}
   368  
   369  	i := int(math.Floor((m.phase + 0.0625) * 8))
   370  	return names[i]
   371  }