|
|
@@ -0,0 +1,355 @@
|
|
|
+/**
|
|
|
+ * This library calculates the current phase of the moon
|
|
|
+ * as well as finds the dates of the recent moon phases.
|
|
|
+ *
|
|
|
+ * Some functionality is ported from python version found here:
|
|
|
+ * https://bazaar.launchpad.net/~keturn/py-moon-phase/trunk/annotate/head:/moon.py
|
|
|
+ *
|
|
|
+ * Some functionality is taken from Astronomical Algorithms, 2nd ed.
|
|
|
+ *
|
|
|
+ * Some functionality is taken from the US Naval Observatory, described here:
|
|
|
+ * https://aa.usno.navy.mil/faq/docs/SunApprox.php
|
|
|
+ *
|
|
|
+ * Author: Ryan Seys (https://github.com/ryanseys)
|
|
|
+ * Author: Jay LaPorte (https://github.com/ironwallaby)
|
|
|
+ */
|
|
|
+
|
|
|
+'use strict'
|
|
|
+
|
|
|
+const julian = require('./julian')
|
|
|
+
|
|
|
+// Phases of the moon & precision
|
|
|
+const NEW = 0
|
|
|
+const FIRST = 1
|
|
|
+const FULL = 2
|
|
|
+const LAST = 3
|
|
|
+const PHASE_MASK = 3
|
|
|
+
|
|
|
+// Astronomical Constants
|
|
|
+// Semi-major axis of Earth's orbit, in kilometers
|
|
|
+const SUN_SMAXIS = 1.49585e8
|
|
|
+
|
|
|
+// SUN_SMAXIS premultiplied by the angular size of the Sun from the Earth
|
|
|
+const SUN_ANGULAR_SIZE_SMAXIS = SUN_SMAXIS * 0.533128
|
|
|
+
|
|
|
+// Semi-major axis of the Moon's orbit, in kilometers
|
|
|
+const MOON_SMAXIS = 384401.0
|
|
|
+
|
|
|
+// MOON_SMAXIS premultiplied by the angular size of the Moon from the Earth
|
|
|
+const MOON_ANGULAR_SIZE_SMAXIS = MOON_SMAXIS * 0.5181
|
|
|
+
|
|
|
+// Synodic month (new Moon to new Moon), in days
|
|
|
+const SYNODIC_MONTH = 29.53058868
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert degrees to radians
|
|
|
+ * @param {Number} d Angle in degrees
|
|
|
+ * @return {Number} Angle in radians
|
|
|
+ */
|
|
|
+function torad (d) {
|
|
|
+ return (Math.PI / 180.0) * d
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert radians to degrees
|
|
|
+ * @param {Number} r Angle in radians
|
|
|
+ * @return {Number} Angle in degrees
|
|
|
+ */
|
|
|
+function dsin (d) {
|
|
|
+ return Math.sin(torad(d))
|
|
|
+}
|
|
|
+
|
|
|
+function dcos (d) {
|
|
|
+ return Math.cos(torad(d))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert astronomical units to kilometers
|
|
|
+ * @param {Number} au Distance in astronomical units
|
|
|
+ * @return {Number} Distance in kilometers
|
|
|
+ */
|
|
|
+function tokm (au) {
|
|
|
+ return 149597870.700 * au
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Finds the phase information for specific date.
|
|
|
+ * @param {Date} date Date to get phase information of.
|
|
|
+ * @return {Object} Phase data
|
|
|
+ */
|
|
|
+function phase (date) {
|
|
|
+ if (!date) {
|
|
|
+ date = new Date()
|
|
|
+ }
|
|
|
+ if (!(date instanceof Date)) {
|
|
|
+ throw new TypeError('Invalid parameter')
|
|
|
+ }
|
|
|
+ if (Number.isNaN(date.getTime())) {
|
|
|
+ throw new RangeError('Invalid Date')
|
|
|
+ }
|
|
|
+
|
|
|
+ // t is the time in "Julian centuries" of 36525 days starting from 2000-01-01
|
|
|
+ // (Note the 0.3 is because the UNIX epoch is on 1970-01-01 and that's 3/10th
|
|
|
+ // of a century before 2000-01-01, which I find cute :3 )
|
|
|
+ const t = date.getTime() * (1 / 3155760000000) - 0.3
|
|
|
+
|
|
|
+ // lunar mean elongation (Astronomical Algorithms, 2nd ed., p. 338)
|
|
|
+ const d = 297.8501921 + t * (445267.1114034 + t * (-0.0018819 + t * ((1 / 545868) + t * (-1 / 113065000))))
|
|
|
+
|
|
|
+ // solar mean anomaly (p. 338)
|
|
|
+ const m = 357.5291092 + t * (35999.0502909 + t * (-0.0001536 + t * (1 / 24490000)))
|
|
|
+
|
|
|
+ // lunar mean anomaly (p. 338)
|
|
|
+ const n = 134.9633964 + t * (477198.8675055 + t * (0.0087414 + t * ((1 / 69699) + t * (-1 / 14712000))))
|
|
|
+
|
|
|
+ // derive sines and cosines necessary for the below calculations
|
|
|
+ const sind = dsin(d)
|
|
|
+ const sinm = dsin(m)
|
|
|
+ const sinn = dsin(n)
|
|
|
+ const cosd = dcos(d)
|
|
|
+ const cosm = dcos(m)
|
|
|
+ const cosn = dcos(n)
|
|
|
+
|
|
|
+ // use trigonometric identities to derive the remainder of the sines and
|
|
|
+ // cosines we need. this reduces us from needing 14 sin/cos calls to only 7,
|
|
|
+ // and makes the lunar distance and solar distance essentially free.
|
|
|
+ // http://mathworld.wolfram.com/Double-AngleFormulas.html
|
|
|
+ // http://mathworld.wolfram.com/TrigonometricAdditionFormulas.html
|
|
|
+ const sin2d = 2 * sind * cosd // sin(2d)
|
|
|
+ const sin2n = 2 * sinn * cosn // sin(2n)
|
|
|
+ const cos2d = 2 * cosd * cosd - 1 // cos(2d)
|
|
|
+ const cos2m = 2 * cosm * cosm - 1 // cos(2m)
|
|
|
+ const cos2n = 2 * cosn * cosn - 1 // cos(2n)
|
|
|
+ const sin2dn = sin2d * cosn - cos2d * sinn // sin(2d - n)
|
|
|
+ const cos2dn = cos2d * cosn + sin2d * sinn // cos(2d - n)
|
|
|
+
|
|
|
+ // lunar phase angle (p. 346)
|
|
|
+ const i = 180 - d - 6.289 * sinn + 2.100 * sinm - 1.274 * sin2dn - 0.658 * sin2d - 0.214 * sin2n - 0.110 * sind
|
|
|
+
|
|
|
+ // fractional illumination (p. 345)
|
|
|
+ const illumination = dcos(i) * 0.5 + 0.5
|
|
|
+
|
|
|
+ // fractional lunar phase
|
|
|
+ let phase = 0.5 - i * (1 / 360)
|
|
|
+ phase -= Math.floor(phase)
|
|
|
+
|
|
|
+ // lunar distance (p. 339-342)
|
|
|
+ // XXX: the book is not clear on how many terms to use for a given level of
|
|
|
+ // accuracy! I used all the easy ones that we already have for free above,
|
|
|
+ // but I imagine this is more than necessary...
|
|
|
+ const moonDistance = 385000.56 - 20905.355 * cosn - 3699.111 * cos2dn - 2955.968 * cos2d - 569.925 * cos2n + 108.743 * cosd
|
|
|
+
|
|
|
+ // solar distance
|
|
|
+ // https://aa.usno.navy.mil/faq/docs/SunApprox.php
|
|
|
+ const sunDistance = tokm(1.00014 - 0.01671 * cosm - 0.00014 * cos2m)
|
|
|
+
|
|
|
+ return {
|
|
|
+ phase: phase,
|
|
|
+ illuminated: illumination,
|
|
|
+ age: phase * SYNODIC_MONTH,
|
|
|
+ distance: moonDistance,
|
|
|
+ angular_diameter: MOON_ANGULAR_SIZE_SMAXIS / moonDistance,
|
|
|
+ sun_distance: sunDistance,
|
|
|
+ sun_angular_diameter: SUN_ANGULAR_SIZE_SMAXIS / sunDistance
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Calculates time of the mean new Moon for a given base date.
|
|
|
+ * This argument K to this function is the precomputed synodic month
|
|
|
+ * index, given by:
|
|
|
+ * K = (year - 1900) * 12.3685
|
|
|
+ * where year is expressed as a year and fractional year.
|
|
|
+ * @param {Date} sdate Start date
|
|
|
+ * @param {[type]} k [description]
|
|
|
+ * @return {[type]} [description]
|
|
|
+ */
|
|
|
+function meanphase (sdate, k) {
|
|
|
+ // Time in Julian centuries from 1900 January 12 noon UTC
|
|
|
+ const delta = (sdate - -2208945600000.0) / 86400000.0
|
|
|
+ const t = delta / 36525
|
|
|
+ return 2415020.75933 +
|
|
|
+ SYNODIC_MONTH * k +
|
|
|
+ (0.0001178 - 0.000000155 * t) * t * t +
|
|
|
+ 0.00033 * dsin(166.56 + (132.87 - 0.009173 * t) * t)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Given a K value used to determine the mean phase of the new moon, and a
|
|
|
+ * phase selector (0, 1, 2, 3), obtain the true, corrected phase time.
|
|
|
+ * @param {[type]} k [description]
|
|
|
+ * @param {[type]} tphase [description]
|
|
|
+ * @return {[type]} [description]
|
|
|
+ */
|
|
|
+function truephase (k, tphase) {
|
|
|
+ // restrict tphase to (0, 1, 2, 3)
|
|
|
+ tphase = tphase & PHASE_MASK
|
|
|
+
|
|
|
+ // add phase to new moon time
|
|
|
+ k = k + 0.25 * tphase
|
|
|
+
|
|
|
+ // Time in Julian centuries from 1900 January 0.5
|
|
|
+ const t = (1.0 / 1236.85) * k
|
|
|
+
|
|
|
+ // Mean time of phase
|
|
|
+ let pt = 2415020.75933 +
|
|
|
+ SYNODIC_MONTH * k +
|
|
|
+ (0.0001178 - 0.000000155 * t) * t * t +
|
|
|
+ 0.00033 * dsin(166.56 + (132.87 - 0.009173 * t) * t)
|
|
|
+
|
|
|
+ // Sun's mean anomaly
|
|
|
+ const m = 359.2242 + 29.10535608 * k - (0.0000333 - 0.00000347 * t) * t * t
|
|
|
+
|
|
|
+ // Moon's mean anomaly
|
|
|
+ const mprime = 306.0253 + 385.81691806 * k + (0.0107306 + 0.00001236 * t) * t * t
|
|
|
+
|
|
|
+ // Moon's argument of latitude
|
|
|
+ const f = 21.2964 + 390.67050646 * k - (0.0016528 - 0.00000239 * t) * t * t
|
|
|
+
|
|
|
+ // use different correction equations depending on the phase being sought
|
|
|
+ switch (tphase) {
|
|
|
+ // new and full moon use one correction
|
|
|
+ case NEW:
|
|
|
+ case FULL:
|
|
|
+ pt += (0.1734 - 0.000393 * t) * dsin(m) +
|
|
|
+ 0.0021 * dsin(2 * m) -
|
|
|
+ 0.4068 * dsin(mprime) +
|
|
|
+ 0.0161 * dsin(2 * mprime) -
|
|
|
+ 0.0004 * dsin(3 * mprime) +
|
|
|
+ 0.0104 * dsin(2 * f) -
|
|
|
+ 0.0051 * dsin(m + mprime) -
|
|
|
+ 0.0074 * dsin(m - mprime) +
|
|
|
+ 0.0004 * dsin(2 * f + m) -
|
|
|
+ 0.0004 * dsin(2 * f - m) -
|
|
|
+ 0.0006 * dsin(2 * f + mprime) +
|
|
|
+ 0.0010 * dsin(2 * f - mprime) +
|
|
|
+ 0.0005 * dsin(m + 2 * mprime)
|
|
|
+ break
|
|
|
+
|
|
|
+ // first and last quarter moon use a different correction
|
|
|
+ case FIRST:
|
|
|
+ case LAST:
|
|
|
+ pt += (0.1721 - 0.0004 * t) * dsin(m) +
|
|
|
+ 0.0021 * dsin(2 * m) -
|
|
|
+ 0.6280 * dsin(mprime) +
|
|
|
+ 0.0089 * dsin(2 * mprime) -
|
|
|
+ 0.0004 * dsin(3 * mprime) +
|
|
|
+ 0.0079 * dsin(2 * f) -
|
|
|
+ 0.0119 * dsin(m + mprime) -
|
|
|
+ 0.0047 * dsin(m - mprime) +
|
|
|
+ 0.0003 * dsin(2 * f + m) -
|
|
|
+ 0.0004 * dsin(2 * f - m) -
|
|
|
+ 0.0006 * dsin(2 * f + mprime) +
|
|
|
+ 0.0021 * dsin(2 * f - mprime) +
|
|
|
+ 0.0003 * dsin(m + 2 * mprime) +
|
|
|
+ 0.0004 * dsin(m - 2 * mprime) -
|
|
|
+ 0.0003 * dsin(2 * m + mprime)
|
|
|
+
|
|
|
+ // the sign of the last term depends on whether we're looking for a first
|
|
|
+ // or last quarter moon!
|
|
|
+ const sign = (tphase < FULL) ? +1 : -1
|
|
|
+ pt += sign * (0.0028 - 0.0004 * dcos(m) + 0.0003 * dcos(mprime))
|
|
|
+
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ return julian.toDate(pt)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Find time of phases of the moon which surround the current date.
|
|
|
+ * Five phases are found, starting and ending with the new moons
|
|
|
+ * which bound the current lunation.
|
|
|
+ * @param {Date} sdate Date to start hunting from (defaults to current date)
|
|
|
+ * @return {Object} Object containing recent past and future phases
|
|
|
+ */
|
|
|
+function phaseHunt (sdate) {
|
|
|
+ if (!sdate) {
|
|
|
+ sdate = new Date()
|
|
|
+ }
|
|
|
+ if (!(sdate instanceof Date)) {
|
|
|
+ throw new TypeError('Invalid parameter')
|
|
|
+ }
|
|
|
+ if (Number.isNaN(sdate.getTime())) {
|
|
|
+ throw new RangeError('Invalid Date')
|
|
|
+ }
|
|
|
+
|
|
|
+ let adate = new Date(sdate.getTime() - (45 * 86400000)) // 45 days prior
|
|
|
+ let k1 = Math.floor(12.3685 * (adate.getFullYear() + (1.0 / 12.0) * adate.getMonth() - 1900))
|
|
|
+ let nt1 = meanphase(adate.getTime(), k1)
|
|
|
+
|
|
|
+ sdate = julian.fromDate(sdate)
|
|
|
+ adate = nt1 + SYNODIC_MONTH
|
|
|
+ let k2 = k1 + 1
|
|
|
+ let nt2 = meanphase(adate, k2)
|
|
|
+ while (nt1 > sdate || sdate >= nt2) {
|
|
|
+ adate += SYNODIC_MONTH
|
|
|
+ k1++
|
|
|
+ k2++
|
|
|
+ nt1 = nt2
|
|
|
+ nt2 = meanphase(adate, k2)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ new_date: truephase(k1, NEW),
|
|
|
+ q1_date: truephase(k1, FIRST),
|
|
|
+ full_date: truephase(k1, FULL),
|
|
|
+ q3_date: truephase(k1, LAST),
|
|
|
+ nextnew_date: truephase(k2, NEW)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function phaseRange (start, end, phase) {
|
|
|
+ if (!(start instanceof Date)) {
|
|
|
+ throw new TypeError('First argument must be a Date object.')
|
|
|
+ }
|
|
|
+ if (Number.isNaN(start.getTime())) {
|
|
|
+ throw new RangeError('First argument not a valid date.')
|
|
|
+ }
|
|
|
+ if (!(end instanceof Date)) {
|
|
|
+ throw new TypeError('Second argument must be a Date object.')
|
|
|
+ }
|
|
|
+ if (Number.isNaN(end.getTime())) {
|
|
|
+ throw new RangeError('Second argument not a valid date.')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (end - start < 0) {
|
|
|
+ let temp = end
|
|
|
+ end = start
|
|
|
+ start = temp
|
|
|
+ }
|
|
|
+
|
|
|
+ start = start.getTime()
|
|
|
+ end = end.getTime()
|
|
|
+
|
|
|
+ let t = start - 45 * 86400000
|
|
|
+
|
|
|
+ let k
|
|
|
+ {
|
|
|
+ const d = new Date(t)
|
|
|
+ k = Math.floor(12.3685 * (d.getFullYear() + (1.0 / 12.0) * d.getMonth() - 1900))
|
|
|
+ }
|
|
|
+
|
|
|
+ let date = truephase(k, phase)
|
|
|
+ // skip every phase before starting date
|
|
|
+ while (date.getTime() < start) {
|
|
|
+ k++
|
|
|
+ date = truephase(k, phase)
|
|
|
+ }
|
|
|
+ // add every phase before (or on!) ending date to a list, and return it
|
|
|
+ const list = []
|
|
|
+ while (date.getTime() <= end) {
|
|
|
+ list.push(date)
|
|
|
+ k++
|
|
|
+ date = truephase(k, phase)
|
|
|
+ }
|
|
|
+ return list
|
|
|
+}
|
|
|
+
|
|
|
+exports.PHASE_NEW = NEW
|
|
|
+exports.PHASE_FIRST = FIRST
|
|
|
+exports.PHASE_FULL = FULL
|
|
|
+exports.PHASE_LAST = LAST
|
|
|
+exports.phase = phase
|
|
|
+exports.phase_hunt = phaseHunt
|
|
|
+exports.phase_range = phaseRange
|