UNPKG

suncalc3

Version:

A tiny JavaScript library for calculating sun/moon positions and phases.

1,241 lines (1,139 loc) 48.3 kB
// @ts-check /* (c) 2011-2015, Vladimir Agafonkin SunCalc is a JavaScript library for calculating sun/moon position and light phases. https://github.com/mourner/suncalc Reworked and enhanced by Robert Gester Additional Copyright (c) 2022 Robert Gester https://github.com/hypnos3/suncalc3 */ /** * @typedef {Object} ISunTimeDef * @property {string} name - The Name of the time * @property {Date} value - Date object with the calculated sun-time * @property {number} ts - The time as timestamp * @property {number} pos - The position of the sun on the time * @property {number} [elevation] - Angle of the sun on the time (except for solarNoon / nadir) * @property {number} julian - The time as Julian calendar * @property {boolean} valid - indicates if the time is valid or not * @property {boolean} [deprecated] - indicates if the time is a deprecated time name * @property {string} [nameOrg] - if it is a deprecated name, the original property name * @property {number} [posOrg] - if it is a deprecated name, the original position */ /** * @typedef {Object} ISunTimeSingle * @property {ISunTimeDef} rise - sun-time for sun rise * @property {ISunTimeDef} set - sun-time for sun set * @property {string} [error] - string of an error message if an error occurs */ /** * @typedef {Object} ISunTimeList * @property {ISunTimeDef} solarNoon - The sun-time for the solar noon (sun is in the highest position) * @property {ISunTimeDef} nadir - The sun-time for nadir (darkest moment of the night, sun is in the lowest position) * @property {ISunTimeDef} goldenHourDawnStart - The sun-time for morning golden hour (soft light, best time for photography) * @property {ISunTimeDef} goldenHourDawnEnd - The sun-time for morning golden hour (soft light, best time for photography) * @property {ISunTimeDef} goldenHourDuskStart - The sun-time for evening golden hour starts * @property {ISunTimeDef} goldenHourDuskEnd - The sun-time for evening golden hour starts * @property {ISunTimeDef} sunriseStart - The sun-time for sunrise starts (top edge of the sun appears on the horizon) * @property {ISunTimeDef} sunriseEnd - The sun-time for sunrise ends (bottom edge of the sun touches the horizon) * @property {ISunTimeDef} sunsetStart - The sun-time for sunset starts (bottom edge of the sun touches the horizon) * @property {ISunTimeDef} sunsetEnd - The sun-time for sunset ends (sun disappears below the horizon, evening civil twilight starts) * @property {ISunTimeDef} blueHourDawnStart - The sun-time for blue Hour start (time for special photography photos starts) * @property {ISunTimeDef} blueHourDawnEnd - The sun-time for blue Hour end (time for special photography photos end) * @property {ISunTimeDef} blueHourDuskStart - The sun-time for blue Hour start (time for special photography photos starts) * @property {ISunTimeDef} blueHourDuskEnd - The sun-time for blue Hour end (time for special photography photos end) * @property {ISunTimeDef} civilDawn - The sun-time for dawn (morning nautical twilight ends, morning civil twilight starts) * @property {ISunTimeDef} civilDusk - The sun-time for dusk (evening nautical twilight starts) * @property {ISunTimeDef} nauticalDawn - The sun-time for nautical dawn (morning nautical twilight starts) * @property {ISunTimeDef} nauticalDusk - The sun-time for nautical dusk end (evening astronomical twilight starts) * @property {ISunTimeDef} amateurDawn - The sun-time for amateur astronomical dawn (sun at 12° before sunrise) * @property {ISunTimeDef} amateurDusk - The sun-time for amateur astronomical dusk (sun at 12° after sunrise) * @property {ISunTimeDef} astronomicalDawn - The sun-time for night ends (morning astronomical twilight starts) * @property {ISunTimeDef} astronomicalDusk - The sun-time for night starts (dark enough for astronomical observations) * @property {ISunTimeDef} [dawn] - Deprecated: alternate for civilDawn * @property {ISunTimeDef} [dusk] - Deprecated: alternate for civilDusk * @property {ISunTimeDef} [nightEnd] - Deprecated: alternate for astronomicalDawn * @property {ISunTimeDef} [night] - Deprecated: alternate for astronomicalDusk * @property {ISunTimeDef} [nightStart] - Deprecated: alternate for astronomicalDusk * @property {ISunTimeDef} [goldenHour] - Deprecated: alternate for goldenHourDuskStart * @property {ISunTimeDef} [sunset] - Deprecated: alternate for sunsetEnd * @property {ISunTimeDef} [sunrise] - Deprecated: alternate for sunriseStart * @property {ISunTimeDef} [goldenHourEnd] - Deprecated: alternate for goldenHourDawnEnd * @property {ISunTimeDef} [goldenHourStart] - Deprecated: alternate for goldenHourDuskStart */ /** * @typedef ISunTimeNames * @type {Object} * @property {number} angle - angle of the sun position in degrees * @property {string} riseName - name of sun rise (morning name) * @property {string} setName - name of sun set (evening name) * @property {number} [risePos] - (optional) position at rise * @property {number} [setPos] - (optional) position at set */ /** * @typedef {Object} ISunCoordinates * @property {number} dec - The declination of the sun * @property {number} ra - The right ascension of the sun */ /** * @typedef {Object} ISunPosition * @property {number} azimuth - The azimuth above the horizon of the sun in radians * @property {number} altitude - The altitude of the sun in radians * @property {number} zenith - The zenith of the sun in radians * @property {number} azimuthDegrees - The azimuth of the sun in decimal degree * @property {number} altitudeDegrees - The altitude of the sun in decimal degree * @property {number} zenithDegrees - The zenith of the sun in decimal degree * @property {number} declination - The declination of the sun */ /** * @typedef {Object} IMoonPosition * @property {number} azimuth - The moon azimuth in radians * @property {number} altitude - The moon altitude above the horizon in radians * @property {number} azimuthDegrees - The moon azimuth in degree * @property {number} altitudeDegrees - The moon altitude above the horizon in degree * @property {number} distance - The distance of the moon to the earth in kilometers * @property {number} parallacticAngle - The parallactic angle of the moon * @property {number} parallacticAngleDegrees - The parallactic angle of the moon in degree */ /** * @typedef {Object} IDateObj * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC */ /** * @typedef {Object} IPhaseObj * @property {number} from - The phase start * @property {number} to - The phase end * @property {('newMoon'|'waxingCrescentMoon'|'firstQuarterMoon'|'waxingGibbousMoon'|'fullMoon'|'waningGibbousMoon'|'thirdQuarterMoon'|'waningCrescentMoon')} id - id of the phase * @property {string} emoji - unicode symbol of the phase * @property {string} name - name of the phase * @property {string} id - phase name * @property {number} weight - weight of the phase * @property {string} css - a css value of the phase * @property {string} [nameAlt] - an alernate name (not used by this library) * @property {string} [tag] - additional tag (not used by this library) */ /** * @typedef {Object} IMoonIlluminationNext * @property {string} date - The Date as a ISO String YYYY-MM-TTTHH:MM:SS.mmmmZ of the next phase * @property {number} value - The Date as the milliseconds since 1.1.1970 0:00 UTC of the next phase * @property {string} type - The name of the next phase [newMoon, fullMoon, firstQuarter, thirdQuarter] * @property {IDateObj} newMoon - Date of the next new moon * @property {IDateObj} fullMoon - Date of the next full moon * @property {IDateObj} firstQuarter - Date of the next first quater of the moon * @property {IDateObj} thirdQuarter - Date of the next third/last quater of the moon */ /** * @typedef {Object} IMoonIllumination * @property {number} fraction - illuminated fraction of the moon; varies from `0.0` (new moon) to `1.0` (full moon) * @property {IPhaseObj} phase - moon phase as object * @property {number} phaseValue - The phase of the moon in the current cycle; varies from `0.0` to `1.0` * @property {number} angle - The midpoint angle in radians of the illuminated limb of the moon reckoned eastward from the north point of the disk; * @property {IMoonIlluminationNext} next - object containing information about the next phases of the moon * @remarks the moon is waxing if the angle is negative, and waning if positive */ /** * @typedef {Object} IMoonDataInst * @property {number} zenithAngle - The zenith angle of the moon * @property {IMoonIllumination} illumination - object containing information about the next phases of the moon * * @typedef {IMoonPosition & IMoonDataInst} IMoonData */ /** * @typedef {Object} IMoonTimes * @property {Date|NaN} rise - a Date object if the moon is rising on the given Date, otherwise NaN * @property {Date|NaN} set - a Date object if the moon is setting on the given Date, otherwise NaN * @property {boolean} alwaysUp - is true if the moon never rises/sets and is always _above_ the horizon during the day * @property {boolean} alwaysDown - is true if the moon is always _below_ the horizon * @property {Date} [highest] - Date of the highest position, only avalílable if set and rise is not NaN */ (function () { 'use strict'; // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas // shortcuts for easier to read formulas const sin = Math.sin; const cos = Math.cos; const tan = Math.tan; const asin = Math.asin; const atan = Math.atan2; const acos = Math.acos; const rad = Math.PI / 180; const degr = 180 / Math.PI; // date/time constants and conversions const dayMs = 86400000; // 1000 * 60 * 60 * 24; const J1970 = 2440587.5; const J2000 = 2451545; const lunarDaysMs = 2551442778; // The duration in days of a lunar cycle is 29.53058770576 const firstNewMoon2000 = 947178840000; // first newMoon in the year 2000 2000-01-06 18:14 /** * convert date from Julian calendar * @param {number} j - day number in Julian calendar to convert * @return {number} result date as timestamp */ function fromJulianDay(j) { return (j - J1970) * dayMs; } /** * get number of days for a dateValue since 2000 * @param {number} dateValue date as timestamp to get days * @return {number} count of days */ function toDays(dateValue) { return ((dateValue / dayMs) + J1970) - J2000; } // general calculations for position const e = rad * 23.4397; // obliquity of the Earth /** * get right ascension * @param {number} l * @param {number} b * @returns {number} */ function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } /** * get declination * @param {number} l * @param {number} b * @returns {number} */ function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } /** * get azimuth * @param {number} H - siderealTime * @param {number} phi - PI constant * @param {number} dec - The declination of the sun * @returns {number} azimuth in rad */ function azimuthCalc(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)) + Math.PI; } /** * get altitude * @param {number} H - siderealTime * @param {number} phi - PI constant * @param {number} dec - The declination of the sun * @returns {number} */ function altitudeCalc(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } /** * side real time * @param {number} d * @param {number} lw * @returns {number} */ function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } /** * get astro refraction * @param {number} h * @returns {number} */ function astroRefraction(h) { if (h < 0) { // the following formula works for positive altitudes only. h = 0; } // if h = -0.08901179 a div/0 would occur. // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); } // general sun calculations /** * get solar mean anomaly * @param {number} d * @returns {number} */ function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } /** * ecliptic longitude * @param {number} M * @returns {number} */ function eclipticLongitude(M) { const C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)); // equation of center const P = rad * 102.9372; // perihelion of the Earth return M + C + P + Math.PI; } /** * sun coordinates * @param {number} d days in Julian calendar * @returns {ISunCoordinates} */ function sunCoords(d) { const M = solarMeanAnomaly(d); const L = eclipticLongitude(M); return { dec: declination(L, 0), ra: rightAscension(L, 0) }; } const SunCalc = {}; /** * calculates sun position for a given date and latitude/longitude * @param {number|Date} dateValue Date object or timestamp for calculating sun-position * @param {number} lat latitude for calculating sun-position * @param {number} lng longitude for calculating sun-position * @return {ISunPosition} result object of sun-position */ SunCalc.getPosition = function (dateValue, lat, lng) { // console.log(`getPosition dateValue=${dateValue} lat=${lat}, lng=${lng}`); if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } if (dateValue instanceof Date) { dateValue = dateValue.valueOf(); } const lw = rad * -lng; const phi = rad * lat; const d = toDays(dateValue); const c = sunCoords(d); const H = siderealTime(d, lw) - c.ra; const azimuth = azimuthCalc(H, phi, c.dec); const altitude = altitudeCalc(H, phi, c.dec); // console.log(`getPosition date=${date}, M=${H}, L=${H}, c=${JSON.stringify(c)}, d=${d}, lw=${lw}, phi=${phi}`); return { azimuth, altitude, zenith: (90*Math.PI/180) - altitude, azimuthDegrees: degr * azimuth, altitudeDegrees: degr * altitude, zenithDegrees: 90 - (degr * altitude), declination: c.dec }; }; /** sun times configuration * @type {Array.<ISunTimeNames>} */ const sunTimes = SunCalc.times = [ { angle: 6, riseName: 'goldenHourDawnEnd', setName: 'goldenHourDuskStart'}, // GOLDEN_HOUR_2 { angle: -0.3, riseName: 'sunriseEnd', setName: 'sunsetStart'}, // SUNRISE_END { angle: -0.833, riseName: 'sunriseStart', setName: 'sunsetEnd'}, // SUNRISE { angle: -1, riseName: 'goldenHourDawnStart', setName: 'goldenHourDuskEnd'}, // GOLDEN_HOUR_1 { angle: -4, riseName: 'blueHourDawnEnd', setName: 'blueHourDuskStart'}, // BLUE_HOUR { angle: -6, riseName: 'civilDawn', setName: 'civilDusk'}, // DAWN { angle: -8, riseName: 'blueHourDawnStart', setName: 'blueHourDuskEnd'}, // BLUE_HOUR { angle: -12, riseName: 'nauticalDawn', setName: 'nauticalDusk'}, // NAUTIC_DAWN { angle: -15, riseName: 'amateurDawn', setName: 'amateurDusk'}, { angle: -18, riseName: 'astronomicalDawn', setName: 'astronomicalDusk'} // ASTRO_DAWN ]; /** alternate time names for backward compatibility * @type {Array.<[string, string]>} */ const suntimesDeprecated = SunCalc.timesDeprecated = [ ['dawn', 'civilDawn'], ['dusk', 'civilDusk'], ['nightEnd', 'astronomicalDawn'], ['night', 'astronomicalDusk'], ['nightStart', 'astronomicalDusk'], ['goldenHour', 'goldenHourDuskStart'], ['sunrise', 'sunriseStart'], ['sunset', 'sunsetEnd'], ['goldenHourEnd', 'goldenHourDawnEnd'], ['goldenHourStart', 'goldenHourDuskStart'] ]; /** adds a custom time to the times config * @param {number} angleAltitude - angle of Altitude/elevation above the horizont of the sun in degrees * @param {string} riseName - name of sun rise (morning name) * @param {string} setName - name of sun set (evening name) * @param {number} [risePos] - (optional) position at rise (morning) * @param {number} [setPos] - (optional) position at set (evening) * @param {boolean} [degree=true] defines if the elevationAngle is in degree not in radians * @return {Boolean} true if new time could be added, false if not (parameter missing; riseName or setName already existing) */ SunCalc.addTime = function (angleAltitude, riseName, setName, risePos, setPos, degree) { let isValid = (typeof riseName === 'string') && (riseName.length > 0) && (typeof setName === 'string') && (setName.length > 0) && (typeof angleAltitude === 'number'); if (isValid) { const EXP = /^(?![0-9])[a-zA-Z0-9$_]+$/; // check for invalid names for (let i=0; i<sunTimes.length; ++i) { if (!EXP.test(riseName) || riseName === sunTimes[i].riseName || riseName === sunTimes[i].setName) { isValid = false; break; } if (!EXP.test(setName) || setName === sunTimes[i].riseName || setName === sunTimes[i].setName) { isValid = false; break; } } if (isValid) { const angleDeg = (degree === false ? (angleAltitude * ( 180 / Math.PI )) : angleAltitude); sunTimes.push({angle: angleDeg, riseName, setName, risePos, setPos}); for (let i = suntimesDeprecated.length -1; i >= 0; i--) { if (suntimesDeprecated[i][0] === riseName || suntimesDeprecated[i][0] === setName) { suntimesDeprecated.splice(i, 1); } } return true; } } return false; }; /** * add an alternate name for a sun time * @param {string} alternameName - alternate or deprecated time name * @param {string} originalName - original time name from SunCalc.times array * @return {Boolean} true if could be added, false if not (parameter missing; originalName does not exists; alternameName already existis) */ SunCalc.addDeprecatedTimeName = function (alternameName, originalName) { let isValid = (typeof alternameName === 'string') && (alternameName.length > 0) && (typeof originalName === 'string') && (originalName.length > 0); if (isValid) { let hasOrg = false; const EXP = /^(?![0-9])[a-zA-Z0-9$_]+$/; // check for invalid names for (let i=0; i<sunTimes.length; ++i) { if (!EXP.test(alternameName) || alternameName === sunTimes[i].riseName || alternameName === sunTimes[i].setName) { isValid = false; break; } if (originalName === sunTimes[i].riseName || originalName === sunTimes[i].setName) { hasOrg = true; } } if (isValid && hasOrg) { suntimesDeprecated.push([alternameName, originalName]); return true; } } return false; }; // calculations for sun times const J0 = 0.0009; /** * Julian cycle * @param {number} d - number of days * @param {number} lw - rad * -lng; * @returns {number} */ function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * Math.PI)); } /** * approx transit * @param {number} Ht - hourAngle * @param {number} lw - rad * -lng * @param {number} n - Julian cycle * @returns {number} approx transit */ function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * Math.PI) + n; } /** * solar transit in Julian * @param {number} ds - approxTransit * @param {number} M - solar mean anomal * @param {number} L - ecliptic longitude * @returns {number} solar transit in Julian */ function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } /** * hour angle * @param {number} h - heigh at 0 * @param {number} phi - rad * lat; * @param {number} dec - declination * @returns {number} hour angle */ function hourAngle(h, phi, dec) { return acos((sin(h) - sin(phi) * sin(dec)) / (cos(phi) * cos(dec))); } /** * calculates the obderver angle * @param {number} height the observer height (in meters) relative to the horizon * @returns {number} height for further calculations */ function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } /** * returns set time for the given sun altitude * @param {number} h - heigh at 0 * @param {number} lw - rad * -lng * @param {number} phi - rad * lat; * @param {number} dec - declination * @param {number} n - Julian cycle * @param {number} M - solar mean anomal * @param {number} L - ecliptic longitude * @returns */ function getSetJ(h, lw, phi, dec, n, M, L) { const w = hourAngle(h, phi, dec); const a = approxTransit(w, lw, n); // console.log(`h=${h} lw=${lw} phi=${phi} dec=${dec} n=${n} M=${M} L=${L} w=${w} a=${a}`); return solarTransitJ(a, M, L); } /** * calculates sun times for a given date and latitude/longitude * @param {number|Date} dateValue Date object or timestamp for calculating sun-times * @param {number} lat latitude for calculating sun-times * @param {number} lng longitude for calculating sun-times * @param {number} [height=0] the observer height (in meters) relative to the horizon * @param {boolean} [addDeprecated=false] if true to times from timesDeprecated array will be added to the object * @param {boolean} [inUTC=false] defines if the calculation should be in utc or local time (default is local) * @return {ISunTimeList} result object of sunTime */ SunCalc.getSunTimes = function (dateValue, lat, lng, height, addDeprecated, inUTC) { // console.log(`getSunTimes dateValue=${dateValue} lat=${lat}, lng=${lng}, height={height}, noDeprecated=${noDeprecated}`); if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } // @ts-ignore const t = new Date(dateValue); if (inUTC) { t.setUTCHours(12, 0, 0, 0); } else { t.setHours(12, 0, 0, 0); } const lw = rad * -lng; const phi = rad * lat; const dh = observerAngle(height || 0); const d = toDays(t.valueOf()); const n = julianCycle(d, lw); const ds = approxTransit(0, lw, n); const M = solarMeanAnomaly(ds); const L = eclipticLongitude(M); const dec = declination(L, 0); const Jnoon = solarTransitJ(ds, M, L); const noonVal = fromJulianDay(Jnoon); const nadirVal = fromJulianDay(Jnoon + 0.5); const result = { solarNoon: { value: new Date(noonVal), ts: noonVal, name: 'solarNoon', // elevation: 90, julian: Jnoon, valid: !isNaN(Jnoon), pos: sunTimes.length }, nadir: { value: new Date(nadirVal), ts: nadirVal, name: 'nadir', // elevation: 270, julian: Jnoon + 0.5, valid: !isNaN(Jnoon), pos: (sunTimes.length * 2) + 1 } }; for (let i = 0, len = sunTimes.length; i < len; i += 1) { const time = sunTimes[i]; const sa = time.angle; const h0 = (sa + dh) * rad; let valid = true; let Jset = getSetJ(h0, lw, phi, dec, n, M, L); if (isNaN(Jset)) { Jset = (Jnoon + 0.5); valid = false; /* Näherung an Wert const b = Math.abs(time[0]); while (isNaN(Jset) && ((Math.abs(sa) - b) < 2)) { sa += 0.005; Jset = getSetJ(sa * rad, lw, phi, dec, n, M, L); } /* */ } const Jrise = Jnoon - (Jset - Jnoon); const v1 = fromJulianDay(Jset); const v2 = fromJulianDay(Jrise); result[time.setName] = { value: new Date(v1), ts: v1, name: time.setName, elevation: sa, julian: Jset, valid, pos: len + i + 1 }; result[time.riseName] = { value: new Date(v2), ts: v2, name: time.riseName, elevation: sa, // (180 + (sa * -1)), julian: Jrise, valid, pos: len - i - 1 }; } if (addDeprecated) { // for backward compatibility for (let i = 0, len = suntimesDeprecated.length; i < len; i += 1) { const time = suntimesDeprecated[i]; result[time[0]] = Object.assign({}, result[time[1]]); result[time[0]].deprecated = true; result[time[0]].nameOrg = result[time[1]].pos; result[time[0]].posOrg = result[time[0]].pos; result[time[0]].pos = -2; } } // @ts-ignore return result; }; /** * calculates the time at which the sun will have a given elevation angle when rising and when setting for a given date and latitude/longitude. * @param {number|Date} dateValue Date object or timestamp for calculating sun-times * @param {number} lat latitude for calculating sun-times * @param {number} lng longitude for calculating sun-times * @param {number} elevationAngle sun angle for calculating sun-time * @param {number} [height=0] the observer height (in meters) relative to the horizon * @param {boolean} [degree] defines if the elevationAngle is in degree not in radians * @param {boolean} [inUTC] defines if the calculation should be in utc or local time (default is local) * @return {ISunTimeSingle} result object of single sunTime */ SunCalc.getSunTime = function (dateValue, lat, lng, elevationAngle, height, degree, inUTC) { // console.log(`getSunTime dateValue=${dateValue} lat=${lat}, lng=${lng}, elevationAngle=${elevationAngle}`); if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } if (isNaN(elevationAngle)) { throw new Error('elevationAngle missing'); } if (degree) { elevationAngle = elevationAngle * rad; } const t = new Date(dateValue); if (inUTC) { t.setUTCHours(12, 0, 0, 0); } else { t.setHours(12, 0, 0, 0); } const lw = rad * -lng; const phi = rad * lat; const dh = observerAngle(height || 0); const d = toDays(t.valueOf()); const n = julianCycle(d, lw); const ds = approxTransit(0, lw, n); const M = solarMeanAnomaly(ds); const L = eclipticLongitude(M); const dec = declination(L, 0); const Jnoon = solarTransitJ(ds, M, L); const h0 = (elevationAngle - 0.833 + dh) * rad; const Jset = getSetJ(h0, lw, phi, dec, n, M, L); const Jrise = Jnoon - (Jset - Jnoon); const v1 = fromJulianDay(Jset); const v2 = fromJulianDay(Jrise); return { set: { name: 'set', value: new Date(v1), ts: v1, elevation: elevationAngle, julian: Jset, valid: !isNaN(Jset), pos: 0 }, rise: { name: 'rise', value: new Date(v2), ts: v2, elevation: elevationAngle, // (180 + (elevationAngle * -1)), julian: Jrise, valid: !isNaN(Jrise), pos: 1 } }; }; /** * calculates time for a given azimuth angle for a given date and latitude/longitude * @param {number|Date} dateValue Date object or timestamp for calculating sun-time * @param {number} nazimuth azimuth for calculating sun-time * @param {number} lat latitude for calculating sun-time * @param {number} lng longitude for calculating sun-time * @param {boolean} [degree] true if the angle is in degree and not in rad * @return {Date} result time of sun-time */ SunCalc.getSunTimeByAzimuth = function (dateValue, lat, lng, nazimuth, degree) { if (isNaN(nazimuth)) { throw new Error('azimuth missing'); } if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } if (degree) { nazimuth = nazimuth * rad; } const date = new Date(dateValue); const lw = rad * -lng; const phi = rad * lat; let dateVal = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0).valueOf(); let addval = dayMs; // / 2); dateVal += addval; while (addval > 200) { // let nazi = this.getPosition(dateVal, lat, lng).azimuth; const d = toDays(dateVal); const c = sunCoords(d); const H = siderealTime(d, lw) - c.ra; const nazim = azimuthCalc(H, phi, c.dec); addval /= 2; if (nazim < nazimuth) { dateVal += addval; } else { dateVal -= addval; } } return new Date(Math.floor(dateVal)); }; // calculation for solar time based on https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time /** * Calculaes the solar time of the given date in the given latitude and UTC offset. * @param {number|Date} dateValue Date object or timestamp for calculating solar time * @param {number} lng longitude for calculating sun-time * @param {number} utcOffset offset to the utc time * @returns {Date} Returns the solar time of the given date in the given latitude and UTC offset. */ SunCalc.getSolarTime = function (dateValue, lng, utcOffset) { // @ts-ignore const date = new Date(dateValue); // calculate the day of year const start = new Date(date.getFullYear(), 0, 0); const diff = (date.getTime() - start.getTime()) + ((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000); const dayOfYear = Math.floor(diff / dayMs); const b = 360 / 365 * (dayOfYear - 81) * rad; const equationOfTime = 9.87 * sin(2 * b) - 7.53 * cos(b) - 1.5 * sin(b); const localSolarTimeMeridian = 15 * utcOffset; const timeCorrection = equationOfTime + 4 * (lng - localSolarTimeMeridian); const localSolarTime = date.getHours() + timeCorrection / 60 + date.getMinutes() / 60; const solarDate = new Date(0, 0); solarDate.setMinutes(+localSolarTime * 60); return solarDate; }; // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas /** * calculate the geocentric ecliptic coordinates of the moon * @param {number} d number of days */ function moonCoords(d) { const L = rad * (218.316 + 13.176396 * d); // ecliptic longitude const M = rad * (134.963 + 13.064993 * d); // mean anomaly const F = rad * (93.272 + 13.229350 * d); // mean distance const l = L + rad * 6.289 * sin(M); // longitude const b = rad * 5.128 * sin(F); // latitude const dt = 385001 - 20905 * cos(M); // distance to the moon in km return { ra: rightAscension(l, b), dec: declination(l, b), dist: dt }; } /** * calculates moon position for a given date and latitude/longitude * @param {number|Date} dateValue Date object or timestamp for calculating moon-position * @param {number} lat latitude for calculating moon-position * @param {number} lng longitude for calculating moon-position * @return {IMoonPosition} result object of moon-position */ SunCalc.getMoonPosition = function (dateValue, lat, lng) { // console.log(`getMoonPosition dateValue=${dateValue} lat=${lat}, lng=${lng}`); if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } if (dateValue instanceof Date) { dateValue = dateValue.valueOf(); } const lw = rad * -lng; const phi = rad * lat; const d = toDays(dateValue); const c = moonCoords(d); const H = siderealTime(d, lw) - c.ra; let altitude = altitudeCalc(H, phi, c.dec); altitude += astroRefraction(altitude); // altitude correction for refraction // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. const pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); const azimuth = azimuthCalc(H, phi, c.dec); return { azimuth, altitude, azimuthDegrees: degr * azimuth, altitudeDegrees: degr * altitude, distance: c.dist, parallacticAngle: pa, parallacticAngleDegrees: degr * pa }; }; const fractionOfTheMoonCycle = SunCalc.moonCycles = [{ from: 0, to: 0.033863193308711, id: 'newMoon', emoji: '🌚', code: ':new_moon_with_face:', name: 'New Moon', weight: 1, css: 'wi-moon-new' }, { from: 0.033863193308711, to: 0.216136806691289, id: 'waxingCrescentMoon', emoji: '🌒', code: ':waxing_crescent_moon:', name: 'Waxing Crescent', weight: 6.3825, css: 'wi-moon-wax-cres' }, { from: 0.216136806691289, to: 0.283863193308711, id: 'firstQuarterMoon', emoji: '🌓', code: ':first_quarter_moon:', name: 'First Quarter', weight: 1, css: 'wi-moon-first-quart' }, { from: 0.283863193308711, to: 0.466136806691289, id: 'waxingGibbousMoon', emoji: '🌔', code: ':waxing_gibbous_moon:', name: 'Waxing Gibbous', weight: 6.3825, css: 'wi-moon-wax-gibb' }, { from: 0.466136806691289, to: 0.533863193308711, id: 'fullMoon', emoji: '🌝', code: ':full_moon_with_face:', name: 'Full Moon', weight: 1, css: 'wi-moon-full' }, { from: 0.533863193308711, to: 0.716136806691289, id: 'waningGibbousMoon', emoji: '🌖', code: ':waning_gibbous_moon:', name: 'Waning Gibbous', weight: 6.3825, css: 'wi-moon-wan-gibb' }, { from: 0.716136806691289, to: 0.783863193308711, id: 'thirdQuarterMoon', emoji: '🌗', code: ':last_quarter_moon:', name: 'third Quarter', weight: 1, css: 'wi-moon-third-quart' }, { from: 0.783863193308711, to: 0.966136806691289, id: 'waningCrescentMoon', emoji: '🌘', code: ':waning_crescent_moon:', name: 'Waning Crescent', weight: 6.3825, css: 'wi-moon-wan-cres' }, { from: 0.966136806691289, to: 1, id: 'newMoon', emoji: '🌚', code: ':new_moon_with_face:', name: 'New Moon', weight: 1, css: 'wi-moon-new' }]; /** * calculations for illumination parameters of the moon, * based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and * Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. * @param {number|Date} dateValue Date object or timestamp for calculating moon-illumination * @return {IMoonIllumination} result object of moon-illumination */ SunCalc.getMoonIllumination = function (dateValue) { // console.log(`getMoonIllumination dateValue=${dateValue}`); if (dateValue instanceof Date) { dateValue = dateValue.valueOf(); } const d = toDays(dateValue); const s = sunCoords(d); const m = moonCoords(d); const sdist = 149598000; // distance from Earth to Sun in km const phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)); const inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)); const angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) - cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)); const phaseValue = 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI; // calculates the difference in ms between the sirst fullMoon 2000 and given Date const diffBase = dateValue - firstNewMoon2000; // Calculate modulus to drop completed cycles let cycleModMs = diffBase % lunarDaysMs; // If negative number (date before new moon 2000) add lunarDaysMs if ( cycleModMs < 0 ) { cycleModMs += lunarDaysMs; } const nextNewMoon = (lunarDaysMs - cycleModMs) + dateValue; let nextFullMoon = ((lunarDaysMs/2) - cycleModMs) + dateValue; if (nextFullMoon < dateValue) { nextFullMoon += lunarDaysMs; } const quater = (lunarDaysMs/4); let nextFirstQuarter = (quater - cycleModMs) + dateValue; if (nextFirstQuarter < dateValue) { nextFirstQuarter += lunarDaysMs; } let nextThirdQuarter = (lunarDaysMs - quater - cycleModMs) + dateValue; if (nextThirdQuarter < dateValue) { nextThirdQuarter += lunarDaysMs; } // Calculate the fraction of the moon cycle // const currentfrac = cycleModMs / lunarDaysMs; const next = Math.min(nextNewMoon, nextFirstQuarter, nextFullMoon, nextThirdQuarter); let phase; for (let index = 0; index < fractionOfTheMoonCycle.length; index++) { const element = fractionOfTheMoonCycle[index]; if ( (phaseValue >= element.from) && (phaseValue <= element.to) ) { phase = element; break; } } return { fraction: (1 + cos(inc)) / 2, // fraction2: cycleModMs / lunarDaysMs, // @ts-ignore phase, phaseValue, angle, next : { value: next, date: (new Date(next)).toISOString(), type: (next === nextNewMoon) ? 'newMoon' : ((next === nextFirstQuarter) ? 'firstQuarter' : ((next === nextFullMoon) ? 'fullMoon' : 'thirdQuarter')), newMoon: { value: nextNewMoon, date: (new Date(nextNewMoon)).toISOString() }, fullMoon: { value: nextFullMoon, date: (new Date(nextFullMoon)).toISOString() }, firstQuarter: { value: nextFirstQuarter, date: (new Date(nextFirstQuarter)).toISOString() }, thirdQuarter: { value: nextThirdQuarter, date: (new Date(nextThirdQuarter)).toISOString() } } }; }; /** * calculations moon position and illumination for a given date and latitude/longitude of the moon, * @param {number|Date} dateValue Date object or timestamp for calculating moon-illumination * @param {number} lat latitude for calculating moon-position * @param {number} lng longitude for calculating moon-position * @return {IMoonData} result object of moon-illumination */ SunCalc.getMoonData = function (dateValue, lat, lng) { const pos = SunCalc.getMoonPosition(dateValue, lat, lng); const illum = SunCalc.getMoonIllumination(dateValue); return Object.assign({ illumination : illum, zenithAngle : illum.angle - pos.parallacticAngle }, pos); }; /** * add hours to a date * @param {number} dateValue timestamp to add hours * @param {number} h - hours to add * @returns {number} new timestamp with added hours */ function hoursLater(dateValue, h) { return dateValue + h * dayMs / 24; } /** * calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article * @param {number|Date} dateValue Date object or timestamp for calculating moon-times * @param {number} lat latitude for calculating moon-times * @param {number} lng longitude for calculating moon-times * @param {boolean} [inUTC] defines if the calculation should be in utc or local time (default is local) * @return {IMoonTimes} result object of sunTime */ SunCalc.getMoonTimes = function (dateValue, lat, lng, inUTC) { if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lng)) { throw new Error('longitude missing'); } const t = new Date(dateValue); if (inUTC) { t.setUTCHours(0, 0, 0, 0); } else { t.setHours(0, 0, 0, 0); } dateValue = t.valueOf(); // console.log(`getMoonTimes lat=${lat} lng=${lng} dateValue=${dateValue} t=${t}`); const hc = 0.133 * rad; let h0 = SunCalc.getMoonPosition(dateValue, lat, lng).altitude - hc; let rise; let set; let ye; let d; let roots; let x1; let x2; let dx; // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) for (let i = 1; i <= 26; i += 2) { const h1 = SunCalc.getMoonPosition(hoursLater(dateValue, i), lat, lng).altitude - hc; const h2 = SunCalc.getMoonPosition(hoursLater(dateValue, i + 1), lat, lng).altitude - hc; const a = (h0 + h2) / 2 - h1; const b = (h2 - h0) / 2; const xe = -b / (2 * a); ye = (a * xe + b) * xe + h1; d = b * b - 4 * a * h1; roots = 0; if (d >= 0) { dx = Math.sqrt(d) / (Math.abs(a) * 2); x1 = xe - dx; x2 = xe + dx; if (Math.abs(x1) <= 1) { roots++; } if (Math.abs(x2) <= 1) { roots++; } if (x1 < -1) { x1 = x2; } } if (roots === 1) { if (h0 < 0) { rise = i + x1; } else { set = i + x1; } } else if (roots === 2) { rise = i + (ye < 0 ? x2 : x1); set = i + (ye < 0 ? x1 : x2); } if (rise && set) { break; } h0 = h2; } const result = {}; if (rise) { result.rise = new Date(hoursLater(dateValue, rise)); } else { result.rise = NaN; } if (set) { result.set = new Date(hoursLater(dateValue, set)); } else { result.set = NaN; } if (!rise && !set) { if (ye > 0) { result.alwaysUp = true; result.alwaysDown = false; } else { result.alwaysUp = false; result.alwaysDown = true; } } else if (rise && set) { result.alwaysUp = false; result.alwaysDown = false; result.highest = new Date(hoursLater(dateValue, Math.min(rise, set) + (Math.abs(set - rise) / 2))); } else { result.alwaysUp = false; result.alwaysDown = false; } return result; }; /** * calc moon transit * @param {number} rize timestamp for rise * @param {number} set timestamp for set time * @returns {Date} new moon transit */ function calcMoonTransit(rize, set) { if (rize > set) { return new Date(set + (rize - set) / 2); } return new Date(rize + (set - rize) / 2); } /** * calculated the moon transit * @param {number|Date} rise rise time as Date object or timestamp for calculating moon-transit * @param {number|Date} set set time as Date object or timestamp for calculating moon-transit * @param {number} lat latitude for calculating moon-times * @param {number} lng longitude for calculating moon-times * @returns {{main: (Date|null), invert: (Date|null)}} */ SunCalc.moonTransit = function (rise, set, lat, lng) { /** @type {Date|null} */ let main = null; /** @type {Date|null} */ let invert = null; const riseDate = new Date(rise); const setDate = new Date(set); const riseValue = riseDate.getTime(); const setValue = setDate.getTime(); const day = setDate.getDate(); let tempTransitBefore; let tempTransitAfter; if (rise && set) { if (rise < set) { main = calcMoonTransit(riseValue, setValue); } else { invert = calcMoonTransit(riseValue, setValue); } } if (rise) { tempTransitAfter = calcMoonTransit(riseValue, SunCalc.getMoonTimes(new Date(riseDate).setDate(day + 1), lat, lng).set.valueOf()); if (tempTransitAfter.getDate() === day) { if (main) { invert = tempTransitAfter; } else { main = tempTransitAfter; } } } if (set) { tempTransitBefore = calcMoonTransit(setValue, SunCalc.getMoonTimes(new Date(setDate).setDate(day - 1), lat, lng).rise.valueOf()); if (tempTransitBefore.getDate() === day) { main = tempTransitBefore; } } return { main, invert }; }; // export as Node module / AMD module / browser variable if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = SunCalc; // @ts-ignore } else if (typeof define === 'function' && define.amd) { // @ts-ignore define(SunCalc); } else { // @ts-ignore window.SunCalc = SunCalc; } })();