SunMoonCalcs.cs

Download license

#region license
/*
(c) 2011-2015, Vladimir Agafonkin, (c) 2016 VPKSoft
Based on a JavaScript library SunCalc for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
Translated to c# by VPKSoft, http://www.vpksoft.net
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SunMoonCalcs
{

    public static class DateTimeJavaScriptExt
    {
        public static double ValueOf(this DateTime dt) // JavaScript Date.valueOf()
        {
            dt = dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : dt;
            return (dt - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
        }

        public static DateTime FromJScriptValue(this DateTime dt, double ms)
        {
            return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ms);
        }
    }


    public class SunMoonCalcs
    {
        private const double dayMs = 86400000;
        private const double J1970 = 2440588;
        private const double J2000 = 2451545;
        private const double PI = Math.PI;
        private const double rad = Math.PI / 180.0;
        private const double e = rad * 23.4397; // obliquity of the Earth

        public class SunTime
        {
            public double Angle { get; set; }
            public string MorningName { get; set; }
            public string EveningName { get; set; }
        }

        public class SunTimeRiseSet: SunTime
        {
            public DateTime RiseTime { get; set; }
            public DateTime SetTime { get; set; }
        }

        // sun times configuration (angle, morning name, evening name)
        public static List<SunTime> SunTimes = new List<SunTime>(new SunTime[] 
        { 
            new SunTime {Angle = -0.833,    MorningName = "sunrise",        EveningName = "sunset" },
            new SunTime {Angle = -0.3,      MorningName = "sunriseEnd",     EveningName = "sunsetStart" },
            new SunTime {Angle = -6,        MorningName = "dawn",           EveningName = "dusk" },
            new SunTime {Angle = -12,       MorningName = "nauticalDawn",   EveningName = "nauticalDusk" },
            new SunTime {Angle = -18,       MorningName = "nightEnd",       EveningName = "night" },
            new SunTime {Angle = 6,         MorningName = "goldenHourEnd",  EveningName = "goldenHour" }
        });

        // adds a custom time to the times config
        public static void AddTime(SunTime sunTime)
        {
            SunTimes.Add(sunTime);
        }

        public class RaDec
        {
            public double ra = 0;
            public double dec = 0;
        }

        public static double ToJulianDate(DateTime dt)
        {
            dt = dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : dt;
            return (dt - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds / dayMs - 0.5 + J1970;
        }

        public static DateTime FromJulianDate(double jd)
        {
            return double.IsNaN(jd) ? DateTime.MinValue : new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((jd + 0.5 - J1970) * dayMs);
        }

        public static double JulianDays(DateTime dt)
        {
            dt = dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : dt;
            return ToJulianDate(dt) - J2000;
        }

        public static double RightAscension(double l, double b)
        {
            return Math.Atan2(Math.Sin(l) * Math.Cos(e) - Math.Tan(b) * Math.Sin(e), Math.Cos(l));
        }

        public static double Declination(double l, double b)
        {
            return Math.Asin(Math.Sin(b) * Math.Cos(e) + Math.Cos(b) * Math.Sin(e) * Math.Sin(l));
        }

        public static double Azimuth(double H, double phi, double dec)
        {
            return Math.Atan2(Math.Sin(H), Math.Cos(H) * Math.Sin(phi) - Math.Tan(dec) * Math.Cos(phi));
        }

        public static double Altitude(double H, double phi, double dec)
        {
            return Math.Asin(Math.Sin(phi) * Math.Sin(dec) + Math.Cos(phi) * Math.Cos(dec) * Math.Cos(H));
        }

        public static double SiderealTime(double d, double lw)
        {
            return rad * (280.16 + 360.9856235 * d) - lw;
        }

        public static double AstroRefraction(double 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
        public static double SolarMeanAnomaly(double d)
        {
            return rad * (357.5291 + 0.98560028 * d);
        }

        public static DateTime HoursLater(DateTime dt, double h)
        {
            return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(dt.ValueOf() + h * dayMs / 24);
        }

        public static double EclipticLongitude(double M)
        {
            double C = rad * (1.9148 * Math.Sin(M) + 0.02 * Math.Sin(2 * M) + 0.0003 * Math.Sin(3 * M)); // equation of center
            double P = rad * 102.9372; // perihelion of the Earth
            return M + C + P + PI;
        }

        public static RaDec SunCoords(double d)
        {
            double M = SolarMeanAnomaly(d);
            double L = EclipticLongitude(M);
            return new RaDec { dec = Declination(L, 0), ra = RightAscension(L, 0) };
        }

        public class MoonRaDecDist
        {
            public double ra = 0;
            public double dec = 0;
            public double dist = 0;
        }

        public static MoonRaDecDist MoonCoords(double d) // geocentric ecliptic coordinates of the moon
        {
            double L = rad * (218.316 + 13.176396 * d); // ecliptic longitude
            double M = rad * (134.963 + 13.064993 * d); // mean anomaly
            double F = rad * (93.272 + 13.229350 * d);  // mean distance

            double l  = L + rad * 6.289 * Math.Sin(M); // longitude
            double b  = rad * 5.128 * Math.Sin(F);     // latitude
            double dt = 385001 - 20905 * Math.Cos(M);  // distance to the moon in km

            return new MoonRaDecDist { ra = RightAscension(l, b), dec = Declination(l, b), dist = dt };
        }

        public class SunCalc
        {
            private const double J0 = 0.0009;

            public class AzAlt
            {
                public double azimuth = 0;
                public double altitude = 0;
            }

            public static AzAlt GetPosition(DateTime dt, double lat, double lng)
            {
                double lw = rad * -lng;
                double phi = rad * lat;
                double d = JulianDays(dt);
                RaDec c = SunCoords(d);
                double H = SiderealTime(d, lw) - c.ra;
                return new AzAlt { azimuth = Azimuth(H, phi, c.dec), altitude = Altitude(H, phi, c.dec) };
            }

            public static double JulianCycle(double d, double lw)
            {
                return Math.Round(d - J0 - lw / (2 * PI));
            }

            public static double ApproxTransit(double Ht, double lw, double n)
            {
                return J0 + (Ht + lw) / (2 * PI) + n;
            }

            public static double SolarTransitJ(double ds, double M, double L)
            {
                return J2000 + ds + 0.0053 * Math.Sin(M) - 0.0069 * Math.Sin(2.0 * L);
            }

            public static double HourAngle(double h, double phi, double d)
            {
                return Math.Acos((Math.Sin(h) - Math.Sin(phi) * Math.Sin(d)) / (Math.Cos(phi) * Math.Cos(d)));
            }

            // returns set time for the given sun altitude
            public static double GetSetJ(double h, double lw, double phi, double dec, double n, double M, double L)
            {
                double w = HourAngle(h, phi, dec);
                double a = ApproxTransit(w, lw, n);
                return SolarTransitJ(a, M, L);
            }
                                                                                                                  // solar disc diameter 
            public static void GetTimes(DateTime dt, double lat, double lng, out DateTime rise, out DateTime set, double angle = -0.833)
            {
                double lw = rad * -lng;
                double phi = rad * lat;
                double d = JulianDays(dt);
                double n = JulianCycle(d, lw);
                double ds = ApproxTransit(0, lw, n);

                double M = SolarMeanAnomaly(ds);
                double L = EclipticLongitude(M);
                double dec = Declination(L, 0);

                double Jnoon = SolarTransitJ(ds, M, L);
                double Jset = GetSetJ(angle * rad, lw, phi, dec, n, M, L);
                double Jrise = Jnoon - (Jset - Jnoon);

                rise = double.IsNaN(Jrise) ? DateTime.MinValue : FromJulianDate(Jrise);
                set = double.IsNaN(Jset) ? DateTime.MinValue : FromJulianDate(Jset);
            }

            public static List<SunTimeRiseSet> GetTimes(DateTime dt, double lat, double lng)
            {
                List<SunTimeRiseSet> retval = new List<SunTimeRiseSet>();
                DateTime rise, set;
                foreach (SunTime st in SunTimes)
                {
                    GetTimes(dt, lat, lng, out rise, out set, st.Angle);
                    retval.Add(new SunTimeRiseSet { Angle = st.Angle, MorningName = st.MorningName, EveningName = st.EveningName, RiseTime = rise, SetTime = set });
                }
                return retval;
            }
        }

        public class MoonCalc
        {
            public class MoonAzAltDistPa
            {
                public double azimuth = 0;
                public double altitude = 0;
                public double distance = 0;
                public double parallacticAngle = 0;
            }

            public class MoonFracPhaseAngle
            {
                public double fraction = 0;
                public double phase = 0;
                public double angle = 0;
            }

            public static MoonAzAltDistPa GetMoonPosition(DateTime dt, double lat, double lng)
            {
                double lw = rad * -lng;
                double phi = rad * lat;
                double d = JulianDays(dt);

                MoonRaDecDist c = MoonCoords(d);
                double H = SiderealTime(d, lw) - c.ra;
                double h = Altitude(H, phi, c.dec);
                // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
                double pa = Math.Atan2(Math.Sin(H), Math.Tan(phi) * Math.Cos(c.dec) - Math.Sin(c.dec) * Math.Cos(H));

                h += AstroRefraction(h); // altitude correction for refraction
                return new MoonAzAltDistPa { azimuth = Azimuth(H, phi, c.dec), altitude = h, distance = c.dist, parallacticAngle = pa };
            }

            public static MoonFracPhaseAngle GetMoonIllumination(DateTime dt)
            {
                double d = JulianDays(dt);
                RaDec s = SunCoords(d);
                MoonRaDecDist m = MoonCoords(d);
                double sdist = 149598000; // distance from Earth to Sun in km
                double phi = Math.Acos(Math.Sin(s.dec) * Math.Sin(m.dec) + Math.Cos(s.dec) * Math.Cos(m.dec) * Math.Cos(s.ra - m.ra));
                double inc = Math.Atan2(sdist * Math.Sin(phi), m.dist - sdist * Math.Cos(phi));
                double angle = Math.Atan2(Math.Cos(s.dec) * Math.Sin(s.ra - m.ra), Math.Sin(s.dec) * Math.Cos(m.dec) -
                               Math.Cos(s.dec) * Math.Sin(m.dec) * Math.Cos(s.ra - m.ra));
                return new MoonFracPhaseAngle { fraction = (1 + Math.Cos(inc)) / 2, phase = 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / PI, angle = angle };
            }

            // DateTime.Max = always up, DateTime.Min = always down
            public static void GetMoonTimes(DateTime dt, double lat, double lng, out DateTime risem, out DateTime setm, out bool ?alwaysUp, out bool ?alwaysDown)
            {
                dt = new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, DateTimeKind.Utc);

                DateTime t = dt;

                double hc = 0.133 * rad;
                double h0 = GetMoonPosition(t, lat, lng).altitude - hc;
                double h1, h2, rise = 0, set = 0, a, b, xe, ye = 0, d, x1, x2, dx;
                int roots;

                for (double i = 1.0; i <= 24.0; i += 2.0)
                {
                    h1 = GetMoonPosition(HoursLater(t, i), lat, lng).altitude - hc;
                    h2 = GetMoonPosition(HoursLater(t, i + 1), lat, lng).altitude - hc;

                    a = (h0 + h2) / 2 - h1;
                    b = (h2 - h0) / 2;
                    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 > 0 && set > 0)
                        {
                            break;
                        }

                        h0 = h2;
                    }
                }

                risem = DateTime.MinValue;
                setm = DateTime.MinValue;

                if (rise > 0)
                {
                    risem = HoursLater(t, rise);
                }

                if (set > 0)
                {
                    setm = HoursLater(t, set);
                }

                alwaysUp = null; 
                alwaysDown = null;

                if (rise < 0 && set < 0)
                {
                    if (ye > 0)
                    {
                        alwaysUp = true;
                        alwaysDown = false;
                        risem = DateTime.MaxValue;
                        setm = DateTime.MaxValue;
                    }
                    else
                    {
                        alwaysDown = true;
                        alwaysUp = false;
                    }
                }
            }
        }
    }
}