<?php
##
## this file name is 'class.solar.php'
##
## solar object -- get sun position or 24 solar terms
##
## [author]
##  - Chilbong Kim, <san2(at)linuxchannel.net>
##  - http://linuxchannel.net/
##
## [changes]
##  - 2009.06.08 : extended, date() to calendar123_widget::_date() of class.caledar.php
##  - 2005.04.17 : add sunrise_sunset()
##  - 2005.02.06 : rebuid deg2valid(), add zodiac()
##  - 2005.01.24 : rebuild time2stime(), atan2()
##  - 2005.01.18 : bug fixed:$RA
##  - 2003.09.08 : bug fixed
##  - 2003.09.06 : new build
##
## [근사식에 대한 신뢰]
##  - 표준편차 : 1289.7736 = 21.5 minutes (standard deviation)
##  - 평균오차 : 817.57409541246 = 13.6 minutes
##  - 최대오차 : +4102.7340(68.4 minutes), -4347.2395(72.5 minutes)
##
## [근사식으로 계산한 24절기 실제 오차] 1902 ~ 2037 년
##  - 표준편차 : 1122.1921 = 18.7 분
##  - 평균오차 : +686.08382175161 = +11.4 분
##  - 최대오차 : +4297.252300024 = +71.6 분, -4278.048699975 = -71.3 분
##  - 최소오차 : +0.16999998688698 = 0 초
##
## [근사식 + 년도 보정으로 계산한 24절기 실제 오차] 1902 ~ 2037 년
##  - 표준편차 : 450.8534 = 7.5 분
##  - 평균오차 : +305.38638890903 = 5.0 분
##  - 최대오차 : +3028.2343000174 = +50.5 분, -1982.9391000271 = -33.1 분
##  - 최소오차 : +0.0085000991821289 = 0 초
##
## [valid date]
##  - 1902.01.01 00:00:00 <= utime <= 2037.12.31 23:59:59
##
## [support date]
##  - unix timestamp base: 1902-01-01 00:00:00 <= date <= 2037-12-31 23:59:59 (guess)
##  - JD(Julian Day) base: BC 4713-01-01 12:00 GMT <= Gregorian date <= AD 9999 (guess)
##
## [download & online source view]
##  - http://ftp.linuxchannel.net/devel/php_solar/
##  - http://ftp.linuxchannel.net/devel/php_calendar/
##
## [demo]
##  - http://linuxchannel.net/gaggle/solar.php
##
## [docs]
##  - http://linuxchannel.net/docs/solar-24terms.txt
##
## [references]
##  - http://cgi.chollian.net/~kohyc/
##  - http://user.chollian.net/~kimdbin/
##  - http://user.chollian.net/~kimdbin/re/calendar.html
##  - http://user.chollian.net/~kimdbin/re/suncoord.html
##  - http://user.chollian.net/~kimdbin/qna/al138.html
##  - http://ruby.kisti.re.kr/~manse/contents-3.html
##  - http://ruby.kisti.re.kr/~anastro/sub_index.htm
##  - http://www-ph.postech.ac.kr/~obs/lecture/lec1/elementary/nakedeyb.htm
##  - http://ruby.kisti.re.kr/~anastro/calendar/etime/ETime.html
##  - http://www.sundu.co.kr/5-information/5-3/5f3-3-5-04earth-1.htm
##  - http://www-ph.postech.ac.kr/~obs/lecture/lec1/elementary/nakedeya.htm
##  - http://upgradename.com/calm.php
##  - http://aa.usno.navy.mil/faq/docs/SunApprox.html
##  - http://aa.usno.navy.mil/data/docs/JulianDate.html
##
## [usage]
##
## [example]
##  require_once 'class.calendar.php';
##  require_once 'class.solar.php';
##  $sun = array();
##  $terms = solar_widget::terms(date('Y'),1,12,&$sun);
##  print_r($terms);
##  print_r($sun);
##  print_r(solar_widget::sun(time()));
##

class solar_widget extends Object
{
  ## check solar terms in today or tomorrow
  ##
  function &solar($utime=0, $GMT=FALSE)
  {
    return solar_widget::today($utime,$GMT);
  }

  function &today($utime=0, $GMT=FALSE)
  {
    if($utime == '') $utime = time();
    if($GMT) $utime -= 32400;

    list($year,$moon,$moonday) = explode(' ',date('Y n nd',$utime));

    $terms = solar_widget::terms($year,$moon,0);

    if($term = $terms[$moonday])
    {
        $str = '오늘은 <B>'.$term.'</B>입니다.';
    }
    else if($term = $terms[date('nd',$utime+86400)])
    {
        $str = '내일은 <B>'.$term.'</B>입니다.';
    }

    return $str;
  }

  ## get sun position at unix timestamp
  ##
  ## [limit]
  ##  - mktime(0,0,0,1,1,1902) < $utime < mktime(23,59,59,12,31,2037)
  ##
  ## [study]
  ##  - w = 23.436
  ##  - tan RA = (sin L * cos w - tan e * sin w ) / cos L
  ##  - sin d = (sin e * cos w) + (cos e * sin w * sin L)
  ##
  ## [example]
  ##  - print_r(solar_widget::sun(mktime(  10,0,0,3,21,2003)  ));
  ##  - print_r(solar_widget::sun(mktime(10-9,0,0,3,21,2003),1)); // same as
  ##
  function &sun($utime, $GMT=FALSE)
  {
    $L = $D = $JD = 0; $J = '';
    $deg2rad = array();

    /***
    if($utime<-2145947400 || $utime>2145884399)
    {
        echo "\nerror: invalid input $utime, 1902.01.01 00:00:00 <= utime <= 2037.12.31 23:59:59\n";
        return -1;
    }
    ***/

    list($L,$atime) = solar_widget::sunl($utime,$GMT,&$D,&$JD,&$J,&$deg2rad);

    ## Sun's ecliptic, in degress
    ##
    $e = sprintf('%.10f',23.439 - (0.00000036*$D)); // degress

    $cosg = cos($deg2rad['g']); // degress
    $cos2g = cos($deg2rad['2g']); // degress

    ## R == AU (sun ~ earth)
    ## The distance of the Sun from the Earth, R, in astronomical units (AU)
    ##
    $R = sprintf('%.10f',1.00014 - (0.01671*$cosg) - (0.00014*$cos2g));

    ## convert
    ##
    $deg2rad['e'] = deg2rad($e); // radian
    $deg2rad['L'] = deg2rad($L); // radian

    $cose = cos($deg2rad['e']); // degress
    $sinL = sin($deg2rad['L']); // degress
    $cosL = cos($deg2rad['L']); // degress
    $sine = sin($deg2rad['e']); // degress

    ## the Sun's right ascension(RA)
     ##
    //$tanRA = sprintf('%.10f',$cose * $sinL / $cosL); // degress
    //$RA = sprintf('%.10f',rad2deg(atan($tanRA)));
    //$RA = $cosL<0 ? $RA+180 : ($sinL<0 ? $RA+360 : $RA); // patch 2005.01.18
    $RA = sprintf('%.10f',rad2deg(atan2($cose*$sinL,$cosL)));
    $RA = solar_widget::deg2valid($RA);

    ## the Sun's declination(d)
    ##
    $sind = sprintf('%.10f',$sine * $sinL); // degress
    $d = sprintf('%.10f',rad2deg(asin($sind))); // Sun's declination, degress

    $solartime = solar_widget::deg2solartime($L);
    $daytime = solar_widget::deg2daytime($RA);

    //if(!($L1=round($L) % 15))
    //{
    //    $idx = $L1 / 15;
    //    list($hterms) = solar_widget::gterms();
    //}

    ## all base degress or decimal
    ##
     return array
    (
    'JD' => $JD,    /*** Julian Day ***/
    'J'  => 'J'.$J, // Jxxxx.xxxx format
    'L'  => $L,    /*** Sun's geocentric apparent ecliptic longitude ***/
    'e'  => $e,    /*** Sun's ecliptic ***/
    'R'  => $R,    /*** Sun from the Earth, astronomical units (AU) ***/
    'RA' => $RA,    /*** Sun's right ascension ***/
    'd'  => $d,    /*** Sun's declination ***/
    'stime'  => $solartime,        /*** solar time ***/
    'dtime'  => $daytime,        /*** day time ***/
    'atime'  => $atime,        /*** append time for integer degress **/
    'utime'  => $utime,        /*** unix timestamp ***/
    'date'   => calendar123_widget::_date('D, d M Y H:i:s T',$utime),    /*** KST date ***/
    'gmdate' => calendar123_widget::_date('D, d M Y H:i:s ',$utime-date('Z')).'GMT',    /*** GMT date ***/
    '_L'  => solar_widget::deg2angle($L),
    '_e'  => solar_widget::deg2angle($e,1),
    '_RA' => solar_widget::deg2angle($RA),
    '_d'  => solar_widget::deg2angle($d,1),
    '_stime' => solar_widget::time2stime($solartime),
    '_dtime' => solar_widget::time2stime($daytime),
    '_atime' => solar_widget::time2stime($atime,TRUE),
    );
  }

  function &sunl($utime, $GMT=FALSE, $D=0, $JD=0, $J='', $deg2rad=array())
  {
    if($GMT) $utime += 32400; // force GMT to static KST, see 946727936

    ## D -- get the number of days from base JD
    ## D = JD(Julian Day) - 2451545.0, base JD(J2000.0)
    ##
    ## base position (J2000.0), 2000-01-01 12:00:00, UT
    ## as   mktime(12,0,0-64,1,1,2000) == 946695536 unix timestamp at KST
    ## as gmmktime(12,0,0-64,1,1,2000) == 946727936 unix timestamp at GMT
    ##
    $D = $utime - 946727936; // number of time
    $D = sprintf('%.10f',$D/86400); // float, number of days
    $JD = sprintf('%.10f',$D+2451545.0); // float, Julian Day
    $J = sprintf('%.4f',2000.0 + ($JD-2451545.0)/365.25); // Jxxxx.xxxx format

    $g = sprintf('%.10f',357.529 + (0.98560028 * $D));
    $q = sprintf('%.10f',280.459 + (0.98564736 * $D));

    ## fixed
    ##
    $g = solar_widget::deg2valid($g); // to valid degress
    $q = solar_widget::deg2valid($q); // to valid degress

    ## convert
    ##
    $deg2rad = array();
    $deg2rad['g'] = deg2rad($g); // radian
    $deg2rad['2g'] = deg2rad($g*2); // radian

    $sing = sin($deg2rad['g']); // degress
    $sin2g = sin($deg2rad['2g']); // degress

    ## L is an approximation to the Sun's geocentric apparent ecliptic longitude
    ##
    $L = sprintf('%.10f',$q + (1.915 * $sing) + (0.020*$sin2g)); 
    $L = solar_widget::deg2valid($L); // degress
    $atime = solar_widget::deg2solartime(round($L)-$L); // float

    return array($L,$atime); // array, float degress, float seconds
  }

  /***
  function &_atime($utime)
  {
    static $unit = 87658.1256;

    list($L) = solar_widget::sunl($utime);
    $td = round($L) - $L;

    while(abs($td) > 0.0000114) // 1/$unit
    {
        $utime += $unit * $td;
        list($L) = solar_widget::sunl($utime);
        $td = round($L) - $L;
    }

    return $utime;
  }
  ***/

  /***
  function &sinl($f, $v)
  {
    return sin(deg2rad($f+$v));
  }

  ## http://linux-sarang.net/board/?p=read&table=qa&no=198189
  ## -2 < sin x < + 2
  ## sin (77 + L) ==> (77 + L - 1.915 sin (77 + L))
  ##
  function &l2d($L)
  {
    $L = (int)$L; // 0 <= $L <= 345
    //$sinl  = sin(deg2rad(77+$L));
    $sinl = solar_widget::sinl($L,77);
    //$sinl = solar_widget::sinl($L-(1.915)*$sinl,77);

    //$sin2l = sin(deg2rad(154+($L*2)));
    $sin2l = solar_widget::sinl($L*2,154);
    $sin2l = solar_widget::sinl($L-(0.020*$L*2*$sin2l),154);
    $sin2l = solar_widget::sinl($L-(0.020*$L*2*$sin2l),154);

    $D = sprintf('%.10f',($L - 280.459 - (1.915 * $sinl) - (0.020 * $sin2l)) / 0.98564736);

    return $D; // float
  }

  function &l2jd($L)
  {
    $D  = solar_widget::l2d($L);
    $JD = sprintf('%.10f',$D+2451545.0);
    //$JD = $JD + ($i * 360 / 0.98564736) 

    return $JD; // float
  }

  function &l2utime($year, $L, $GMT=FLASE)
  {
    $i = (int)$year - 2000 + 1;
    $D = solar_widget::l2d($L);

    $utime = ($D * 86400) + 946727936 + (31556925.216 * $i);
    $utime = round($utime);

    return $utime - ($GMT ? 32400 : 0); // integer
  }
  ***/

  ## 1 hour == 15 degress
  ## 1 degress == 4 minute == 240 seconds
  ##
  function &deg2daytime($deg)
  {
    return sprintf('%.4f',$deg*240); // seconds
  }

  ## 1 solar year == 365.242190 days == 31556925.216 seconds
  ## 1 degress == 31556925.216 seconds / 360 degress == 87658.1256 seconds
  ##
  function &deg2solartime($deg)
  {
    return sprintf('%.4f',$deg*87658.1256); // seconds
  }

  function &deg2angle($deg, $singed=FALSE)
  {
    if($singed) $singed = '+';
    if($deg <0) { $singed = '-'; $deg = abs($deg); }

    $time = sprintf('%.4f',$deg*3600);
    $degr = (int)$deg.chr(161).chr(198); //sprintf('%d',$deg);
    $time = sprintf('%.4f',$time-($degr*3600)); // fmod
    $mins = sprintf('%02d',$time/60).chr(161).chr(199);
    $secs = sprintf('%.4f',$time-($mins*60)).chr(161).chr(200); // fmod

    return $singed.$degr.$mins.$secs;
  }

  function &deg2valid($deg)
  {
    //if($deg <= 360 && $deg >=0) return $deg;
    $deg = ($deg>=0) ? fmod($deg,360) : fmod($deg,360)+360.0;

    return (float)$deg; // float degress
  }

  function &moon2valid($moon)
  {
    //$moon = max($moon,1);
    //$moon = min($moon,12);

    if($moon < 1) $moon = 1;
    else if($moon > 12) $moon = 12;

    return (int)$moon;
  }

  function &time2stime($time, $singed=FALSE)
  {
    if($singed) $singed = '+';
    if($time<0) { $singed = '-'; $time = abs($time); }

    return $singed.calendar123_widget::_date('z H i s',$time-date('Z'));
  }

  function &gterms()
  {
    static $hterms = array
    (
    '소한','대한','입춘','우수','경칩','춘분','청명','곡우',
    '입하','소만','망종','하지','소서','대서','입추','처서',
    '백로','추분','한로','상강','입동','소설','대설','동지'
    );

    static $tterms = array
    (
    -6418939, -5146737, -3871136, -2589569, -1299777,        0,
     1310827,  2633103,  3966413,  5309605,  6660762,  8017383,
     9376511, 10735018, 12089855, 13438199, 14777792, 16107008,
    17424841, 18731368, 20027093, 21313452, 22592403, 23866369
    );

    ## mktime(7+9,36,19-64,3,20,2000), 2000-03-20 16:35:15(KST)
    ##
    if(!defined('__SOLAR_START__'))
    {
    define('__SOLAR_START__',953537715); // start base unix timestamp
    define('__SOLAR_TYEAR__',31556940); // tropicalyear to seconds
    define('__SOLAR_BYEAR__',2000); // start base year
    }

    return array($hterms,$tterms);
  }

  function &tterms($year)
  {
    static $addstime = array
    (
    1902=> 1545, 1903=> 1734, 1904=> 1740, 1906=>  475, 1907=>  432,
    1908=>  480, 1909=>  462, 1915=> -370, 1916=> -332, 1918=> -335,
    1919=> -263, 1925=>  340, 1927=>  344, 1928=> 2133, 1929=> 2112,
    1930=> 2100, 1931=> 1858, 1936=> -400, 1937=> -400, 1938=> -342,
    1939=> -300, 1944=>  365, 1945=>  380, 1946=>  400, 1947=>  200,
    1948=>  244, 1953=> -266, 1954=> 2600, 1955=> 3168, 1956=> 3218,
    1957=> 3366, 1958=> 3300, 1959=> 3483, 1960=> 2386, 1961=> 3015,
    1962=> 2090, 1963=> 2090, 1964=> 2264, 1965=> 2370, 1966=> 2185,
    1967=> 2144, 1968=> 1526, 1971=> -393, 1972=> -430, 1973=> -445,
    1974=> -543, 1975=> -393, 1980=>  300, 1981=>  490, 1982=>  400,
    1983=>  445, 1984=>  393, 1987=>-1530, 1988=>-1600, 1990=> -362,
    1991=> -366, 1992=> -400, 1993=> -449, 1994=> -321, 1995=> -344,
    1999=>  356, 2000=>  480, 2001=>  483, 2002=>  504, 2003=>  294,
    2007=> -206, 2008=> -314, 2009=> -466, 2010=> -416, 2011=> -457,
    2012=> -313, 2018=>  347, 2020=>  257, 2021=>  351, 2022=>  159,
    2023=>  177, 2026=> -134, 2027=> -340, 2028=> -382, 2029=> -320,
    2030=> -470, 2031=> -370, 2032=> -373, 2036=>  349, 2037=>  523,
    );

    static $addttime = array
    (
    1919=> array(14=>-160), 1939=> array(10=> -508),
    1953=> array( 0=> 220), 1954=> array( 1=>-2973),
    1982=> array(18=> 241), 1988=> array(13=>-2455),
    2013=> array( 6=> 356), 2031=> array(20=>  411),
    2023=> array( 0=>  399, 11=>-571),
    );

    return array($addstime[$year],$addttime[$year]);
  }

  ## get the 24 solar terms, 1902 ~ 2037
  ##
  ## [usage]
  ##  - array solar_widget::terms(int year [, int smoon [, int length [, array &sun]]] )
  ##
  function &terms($year=0, $smoon=1, $length=12, $sun=array())
  {
    $year  = (int)$year;
    $sun = array();
    $smoon = (int)$smoon;
    $length = (int)$length;
    $times = array();

    if(!$year) $year = date('Y');

    /***
    if($year<1902 || $year>2037)
    {
        echo "\nerror: invalid input $year, 1902 <= year <= 2037\n";
        return -1;
    }
    ***/

    list($hterms,$tterms) = solar_widget::gterms();
    list($addstime,$addttime) = solar_widget::tterms($year);

    ## mktime(7+9,36,19-64,3,20,2000), 2000-03-20 16:35:15(KST)
    ##
    $start = __SOLAR_START__; // start base unix timestamp
    $tyear = __SOLAR_TYEAR__; // tropicalyear to seconds
    $byear = __SOLAR_BYEAR__; // start base year

    $start += ($year - $byear) * $tyear;

    if($length < -12) $length = -12;
    else if($length > 12) $length = 12;

    $smoon = solar_widget::moon2valid($smoon);
    $emoon = solar_widget::moon2valid($smoon+$length);

    $sidx =  (min($smoon,$emoon) - 1) * 2;
    $eidx = ((max($smoon,$emoon) - 1) * 2) + 1;

    for($i=$sidx; $i<=$eidx; $i++)
    {
        $time = $start + $tterms[$i];
        list(,$atime) = solar_widget::sunl($time,FALSE);
        $time += $atime + $addstime + $addttime[$i]; // re-fixed
        $terms[calendar123_widget::_date('nd',$time)] = &$hterms[$i];
        $times[] = $time; // fixed utime
    }

    ## for detail information
    ##
    if(func_num_args() > 3)
    {
        $i = $sidx;
        foreach($times AS $time)
        {
            $sun[$i] = solar_widget::sun($time,FALSE);
            $sun[$i]['_avgdate'] = calendar123_widget::_date('D, d M Y H:i:s ',$start+$tterms[$i]-date('Z')).'GMT';
            $sun[$i]['_name'] = &$hterms[$i];
            $i++;
        }
    }

    unset($times);

    return $terms; // array
  }

  ## public, get a Constellation of zodiac
  ##
  function &zodiac($y, $m, $d)
  {
    static $ffd = array // patch day
    (
    19030622 => -24, 19221222 =>   4, 19540420 => -61, 19550723 => -48,
    19551222 => -57, 19560320 => -48, 19580823 => -52, 19600219 => -55,
    19610221 => -59, 19620823 => -37, 19651023 => -42, 19870525 =>  64,
    19880722 =>  61, 20230621 =>   4, 20300218 =>   7
    );

    $horoscope = array // do not set `static' variable, it's a bug?
    (
    array(chr(191).chr(176).chr(188).chr(210),'Capricon'),
    array(chr(200).chr(178).chr(188).chr(210),'Taurus'),
    array(chr(189).chr(214).chr(181).chr(213).chr(192).chr(204),'Gemini'),
    array(chr(176).chr(212),'Cancer'),
    array(chr(187).chr(231).chr(192).chr(218),'Leo'),
    array(chr(195).chr(179).chr(179).chr(224),'Virgo'),
    array(chr(195).chr(181).chr(196).chr(170),'Libra'),
    array(chr(192).chr(252).chr(176).chr(165),'Scorpius'),
    array(chr(177).chr(195).chr(188).chr(246),'Sagittarius'),
    array(chr(187).chr(234).chr(190).chr(231),'Aries'),
    array(chr(185).chr(176).chr(186).chr(180),'Aquarius'),
    array(chr(185).chr(176).chr(176).chr(237).chr(177).chr(226),'Pisces')
    );

    //list(,$nd) = _solar_widget::_terms($y,$m,0);
    //$idx = ($m.$d<$nd) ? $m-2 : $m-1;
    //if($idx < 0) $idx += 12;

    $fk = sprintf('%d%02d%d',$y,$m,$d);
    list($L) = solar_widget::sunl(calendar123_widget::_mktime(23,59+(int)$ffd[$fk],59,$m,$d,$y));

    return $horoscope[floor($L/30)];
  }

  ## public, get sunrise, sunset on Korea
  ##
  ## same as PHP5 `date_sunrise()', `date_sunset()'
  ## http://williams.best.vwh.net/sunrise_sunset_example.htm
  ## http://kr.php.net/manual/en/function.date-sunrise.php
  ##
  ## [zenith]
  ##  offical      = 90 degrees 50'
  ##  civil        = 96 degrees
  ##  nautical     = 102 degrees
  ##  astronomical = 108 degrees
  ##
  function sunrise_sunset($_y, $_m, $_d, $_location=0, $_zenith=0)
  {
    static $_locations = array
    (
    array(126.95,37.55)/* 서울 */, array(131.87,37.24)/* 독도 */,
    array(129.37,36.04)/* 포항 */, array(126.35,36.52)/* 안면 */,
    );

    static $_zeniths = array(90.8333, 96.0, 102.0, 108.0);
    static $_timezone = 9.0; // KST +9H

    ## check arguments
    ##
    if(!preg_match('/^[0-3]$/',$_location)) $_location = 0;
    if(!preg_match('/^[0-3]$/',$_zenith)) $_zenith = 0;

    ## inital configurations
    ##
    $location  = $_locations[$_location];
    $longitude = $location[0];
    $latitude  = deg2rad($location[1]);
    $zenith    = deg2rad($_zeniths[$_zenith]);

    ## 1. first calculate the day of the year
    ##
    $N = floor(275*$_m/9) - (floor(($_m+9)/12) * (1+floor(($_y-4*floor($_y/4)+2)/3))) + $_d - 30;

    ## 2. convert the longitude to hour value and calculate an approximate time
    ##
    $lhour = $longitude / 15;
    $t['r'] = sprintf('%.8f',$N+((6-$lhour)/24.0)); // sunrise
    $t['s'] = sprintf('%.8f',$N+((18-$lhour)/24.0)); // sunrise

    ## 3. calculate the Sun's mean anomaly
    ##
    $M['r'] = (0.9856*$t['r']) - 3.289;
    $M['s'] = (0.9856*$t['s']) - 3.289;

    ## 4. calculate the Sun's true longitude
    ## to be adjusted into the range [0,360) by adding/subtracting 360
    ##
    $L['r'] = $M['r'] + (1.916*sin(deg2rad($M['r']))) + (0.020*sin(deg2rad(2*$M['r']))) + 282.634;
    $L['s'] = $M['s'] + (1.916*sin(deg2rad($M['s']))) + (0.020*sin(deg2rad(2*$M['s']))) + 282.634;
    $L['r'] = ($L['r']>=0) ? fmod($L['r'],360) : fmod($L['r'],360)+360.0;
    $L['s'] = ($L['s']>=0) ? fmod($L['s'],360) : fmod($L['s'],360)+360.0;
    $l['r'] = deg2rad($L['r']);
    $l['s'] = deg2rad($L['s']);

    ## 5a. calculate the Sun's right ascension
    ## to be adjusted into the range [0,360) by adding/subtracting 360
    ##
    $RA['r'] = rad2deg(atan(0.91764*tan($l['r'])));
    $RA['s'] = rad2deg(atan(0.91764*tan($l['s'])));
    $RA['r'] = ($RA['r']>=0) ? fmod($RA['r'],360) : fmod($RA['r'],360)+360.0;
    $RA['s'] = ($RA['s']>=0) ? fmod($RA['s'],360) : fmod($RA['s'],360)+360.0;

    ## 5b. right ascension value needs to be in the same quadrant as L
    ##
    $RA['r'] += (floor($L['r']/90.0)*90.0) - (floor($RA['r']/90.0)*90.0);
    $RA['s'] += (floor($L['s']/90.0)*90.0) - (floor($RA['s']/90.0)*90.0);

    ## 5c. right ascension value needs to be converted into hours
    ##
    $RA['r'] /= 15;
    $RA['s'] /= 15;

    ## 6. calculate the Sun's declination
    ##
    $sindec['r'] = 0.39782 * sin($l['r']);
    $sindec['s'] = 0.39782 * sin($l['s']);
    $cosdec['r'] = cos(asin($sindec['r']));
    $cosdec['s'] = cos(asin($sindec['s']));

    ## 7a. calculate the Sun's local hour angle
    ## (cosH> 1) the sun never rises on this location (on the specified date)
    ## (cosH<-1) the sun never sets on this location (on the specified date)
    ##
    $cosH['r'] = (cos($zenith) - ($sindec['r']*sin($latitude))) / ($cosdec['r']*cos($latitude));
    $cosH['s'] = (cos($zenith) - ($sindec['s']*sin($latitude))) / ($cosdec['s']*cos($latitude));

    ## 7b. finish calculating H and convert into hours
    ##
    $H['r'] = 360.0 - rad2deg(acos($cosH['r']));
    $H['s'] = rad2deg(acos($cosH['s']));
    $H['r'] /= 15;
    $H['s'] /= 15;

    ## 8. calculate local mean time of rising/setting
    ##
    $T['r'] = $H['r'] + $RA['r'] - (0.06571*$t['r']) - 6.622;
    $T['s'] = $H['s'] + $RA['s'] - (0.06571*$t['s']) - 6.622;

    ## 9. adjust back to UTC
    ## to be adjusted into the range [0,24) by adding/subtracting 24
    ##
    $UT['r'] = $T['r'] - $lhour;
    $UT['s'] = $T['s'] - $lhour;
    $UT['r'] = ($UT['r']>=0) ? fmod($UT['r'],24.0) : fmod($UT['r'],24.0) + 24.0;
    $UT['s'] = ($UT['s']>=0) ? fmod($UT['s'],24.0) : fmod($UT['s'],24.0) + 24.0;

    ## 10. convert UT value to local time zone of latitude/longitude
    ##
    $localT['r'] = fmod($UT['r']+$_timezone,24.0);
    $localT['s'] = fmod($UT['s']+$_timezone,24.0);

    ## last convert localT to human time
    ##
    $sunrise['H'] = floor($localT['r']);
    $sunrise['m'] = (int)(($localT['r']-$sunrise['H'])*60);
    $sunset['H'] = floor($localT['s']);
    $sunset['m'] = (int)(($localT['s']-$sunset['H'])*60);

    return array
    (
    sprintf('%02d',$sunrise['H']).':'.sprintf('%02d',$sunrise['m']), // sunrise HH:MM
    sprintf('%02d',$sunset['H']).':'.sprintf('%02d',$sunset['m']), // sunset HH:MM
    );
  }
} // end of class

/*** example ***
require_once 'class.calendar.php';
require_once 'class.solar.php';
$sun = array();
$terms = solar_widget::terms(date('Y'),1,12,&$sun);
print_r($terms);
print_r($sun);
print_r(solar_widget::sun(time()));
echo solar_widget::today()."\n";
echo solar_widget::solar(mktime(0,0,0,3,20))."\n";
echo solar_widget::solar(mktime(0,0,0,3,21))."\n";
echo solar_widget::solar(mktime(0,0,0,3,22))."\n";
echo "\n\n";
print_r(solar_widget::terms(2023));
***/
?>