<?php

/*
   This program simply computes the basic ephemeris statistics and
   simple phase of the moon for any date, time and location over a
   span of several thousands of years.

   The computations are based on the NASA/JPL Horizons API.

   Range:
   BC 9999-MAR-21 00:00:00.0006 TT  to  AD 9999-DEC-30 12:00:00.0011 TT
   BC 9999-MAR-15 21:49:54.7144 UT  to  AD 9999-DEC-30 11:58:50.8171 UT

   NOT ALL ARGUMENT ERRORS ARE CAUGHT BY THIS INTERFACE. SOME ARE RUN
   TIME ERRORS ONLY DETECTED WHEN PASSED TO THE NASA/JPL HORIZONS API
   WHICH SHOULD CATCH AND REPORT THEM.

   Author   : Jay Tanner - 2024
   Language : PHP v7.4.9
   License  : Public Domain
*/

// --------------------------------------------------------
// Initialize output buffer.

   ob_start();

// --------------------------------------
// Current year (from system clock time).

   $cYear = date('Y');

// ----------------------------------------------------------------
// Define the program cookie name and set it to expire in 30 days.

   $CookieName = '20000-Year-Lunar-Phase-Calculator';
   $ExpiresIn30Days = time() + 30*86400;

// ------------------------------------------------
// Define author, working script path and filename.

   $_AUTHOR_           = "Jay Tanner";
   $_PROGRAM_VERSION_  = "v1.0 - ";
                   $at = "&#97;&#116;&#32;&#76;&#111;&#99;&#97;&#108;&#32;&#84;&#105;&#109;&#101;&#32;";
                  $LTC = "&#85;&#84;&#67;";
   $_SCRIPT_PATH_      = Filter_Input(INPUT_SERVER, 'SCRIPT_FILENAME');

// -----------------------------------------
// Define internal document page title, HTML
// page heading text and revision date

   $_BROWSER_TAB_TEXT_ = "20,000-Year Lunar Phase Calculator";
   $_INTERFACE_TITLE_  = "\n<span style='font-size:15pt;'>20,000-Year Lunar Phase Calculator</span><br>\n<span style='font-size:11pt; line-height:175%;'>BC&nbsp;9999-Mar-21-Thu&nbsp;&nbsp;00:00:00.0006&nbsp;TT&nbsp;&nbsp;to&nbsp;&nbsp;AD&nbsp;9999-Dec-30-Thu&nbsp;&nbsp;12:00:00.0011&nbsp;TT<br>or<br>
BC&nbsp;9999-Mar-15-Thu&nbsp;&nbsp;21:49:54.7144&nbsp;UT&nbsp;&nbsp;to&nbsp;&nbsp;AD&nbsp;9999-Dec-30-Thu&nbsp;&nbsp;11:58:50.8171&nbsp;UT
</span><br>\n<span style='font-size:10pt;'>PHP Program by $_AUTHOR_ - $cYear - Built Around The NASA/JPL Horizons API</span>\n";
   $_REVISION_DATE_    = $_PROGRAM_VERSION_ .'Revised: '. date("Y-F-d-l $at h:i:s A   ($LTC", FileMTime($_SCRIPT_PATH_))."&minus;05:00)";

// Define JavaScript message to display while working.
// This is attached to the [COMPUTE] button.
   $_COMPUTING_ = "TextArea1.innerHTML='    W.O.R.K.I.N.G --- This may take several seconds.';";

// -------------------------------------
// Define main TextArea text, background
// colors and default HTML column span.

   $TxColor = 'black';
   $BgColor = 'white';
   $ColSpan = 99;

   $PhaseAngImg = 'error.png';

// -------------------------------------------
// Define default home location coordinates
// in degrees and elevation in kilometers.
// If you have a talking homing pigeon, ask
// it for your home coordinates or ask Google.
// -------------------------------------------


   $HomeLoc   = 'Central New York State, USA';
   $HomeTZ    = '-05:00';     // Negative = West
   $HomeLon   = '-76.862737'; // Negative = West
   $LonDeg    = $HomeLon;
   $HomeLat   = '+42.904788'; // Negative = South
   $LatDeg    = $HomeLat;
   $HomeAlt   = '+150';       // Meters - Relative to Sea-Level

/*
   $HomeLoc   = 'New York City, USA';
   $HomeTZ    = '-05:00'; // Negative = West
   $HomeLon   = $LatDeg = '-73.935242'; // Negative = West
   $LonDeg    = $HomeLon;
   $HomeLat   = $LonDeg = '+40.730610'; // Negative = South
   $LatDeg    = $HomeLat;
   $HomeAlt   = '+53'; // Meters - Relative to Sea-Level
*/





// -------------------------------------------------
// Do this only if the [COMPUTE] button was clicked.

   $w = Filter_Input(INPUT_POST, 'ComputeButton');
   if (!IsSet($w))
     {

// -----------------------------------------------------
// If [COMPUTE]  button was clicked AND an active cookie
// exists, then restore the  previous interface settings
// from it. Otherwise, set the default startup values.

       $w = Filter_Input(INPUT_COOKIE, $CookieName);
            if (IsSet($w))
      {
       $CookieDataString = Filter_Input(INPUT_COOKIE, $CookieName);
        list
       (
        $LocName,
        $TimeZone,
        $DSSTYN,
        $StartBCAD,
        $StartYear,
        $StartMonth,
        $StartDay,
        $StartTime,
        $TimeScale,
        $Longitude,
        $Latitude,
        $AltM
       ) = Preg_Split("[\|]", $CookieDataString);

        $BodyID = '301';
        $LonDeg = trim($Longitude);
        $LatDeg = trim($Latitude);
      }

   else

// -------------------------------------------------
// SET THE INITIAL DEFAULT INTERFACE STARTUP VALUES.

 {
  $BodyID       = '301'; // Force to Moon only
  $LocName      = $HomeLoc;
  $TimeZone     = $HomeTZ;
  $DSSTYN        = 'No';
  $StartBCAD    = 'AD';
  $StartYear    = date('Y');
  $StartMonth   = date('M');
  $StartDay     = date('d');
  $StartTime    = date('H:i:s');
  $TimeScale    = 'UT';
  $Longitude    = $HomeLon;
  $LonDeg       = $HomeLon;
  $Latitude     = $HomeLat;
  $LatDeg       = $HomeLat;
  $AltM         = $HomeAlt;

// -----------------------------------
// Store interface settings in cookie.

   $CookieDataString = "$LocName|$TimeZone|$DSSTYN|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$TimeScale|$Longitude|$Latitude|$AltM";
   SetCookie ($CookieName, $CookieDataString, $ExpiresIn30Days);
  }
      } // End of  if (!IsSet(...))




// ---------------------------------------------
// READ VALUES OF ALL INTERFACE ARGUMENTS. EMPTY
// VALUES ARE SET TO THE DEFINED DEFAULT VALUES.
//
// NOTE:
// The @ symbols suppress the warning messages,
// which are actually unnecessary in this case.

   $w = Filter_Input(INPUT_POST, 'ComputeButton');
   if (isset($w))
{
   $BodyID = '301'; // Forced to moon (Luna) only.

   $LocName = trim(Filter_Input(INPUT_POST, 'LocName'));
   if ($LocName == '') {$LocName = '-';}

   $TimeZone = trim(Filter_Input(INPUT_POST, 'TimeZone'));
   if ($TimeZone == '') {$TimeZone = $HomeTZ;}
@  $TimeZone = $TZHours = HMS_to_Hours($TimeZone);
@  $TimeZone = substr(Hours_to_HMS($TimeZone, 0, '+', ':'),0,6);

   $DSSTYN = Substr(StrToUpper(trim(Filter_Input(INPUT_POST, 'DSTYN'))),0,1);
   if ($DSSTYN == '') {$DSSTYN = 'N';}
   $DSSTYN  = ($DSSTYN == 'N')? 'No':'Yes';

   $StartBCAD = Substr(StrToUpper(trim(Filter_Input(INPUT_POST, 'StartBCAD'))),0,1);
   if ($StartBCAD == '') {$StartBCAD = 'AD';}
   $StartBCAD = ($StartBCAD == 'B')? 'BC':'AD';

   $StartYear = trim(Filter_Input(INPUT_POST, 'StartYear'));
   if ($StartYear == '') {$StartYear = date('Y');}
   if (Is_Numeric($StartYear)) {SPrintF("%04d", $StartYear);}

   $StartMonth = trim(Filter_Input(INPUT_POST, 'StartMonth'));
   if ($StartMonth == '') {$StartMonth = date('M');}
   $StartMonth = Month_To_Str($StartMonth);

   $StartDay = trim(Filter_Input(INPUT_POST, 'StartDay'));
   if ($StartDay == '') {$StartDay = date('d');}
   if (Is_Numeric($StartDay)) {$StartDay = SPrintF("%02d", $StartDay);}

   $StartTime = trim(Filter_Input(INPUT_POST, 'StartTime'));
   if ($StartTime == '') {$StartTime = date('H:i:s');}
@  $StartTime = HMS_to_Hours($StartTime);
@  $StartTime = Hours_to_HMS($StartTime, 0, '', ':');

   $TimeScale = StrToUpper(substr(trim(Filter_Input(INPUT_POST, 'TimeScale')),0,1));
   if ($TimeScale == '') {$TimeScale = 'UT';}
   $TimeScale = ($TimeScale == 'T')? 'TT':'UT';

   $Longitude = trim(Filter_Input(INPUT_POST, 'Longitude'));
   if ($Longitude == '') {$Longitude = $HomeLon;}
@  $Longitude = DMS_to_Deg($Longitude);
   $LonDeg = $Longitude;
   $Longitude = SPrintF("%+1.9f", $Longitude);

   $Latitude = trim(Filter_Input(INPUT_POST, 'Latitude'));
   if ($Latitude == '') {$Latitude = $HomeLat;}
@  $Latitude = DMS_to_Deg($Latitude);
   $LatDeg = $Latitude;
   $Latitude = SPrintF("%+1.9f", $Latitude);

   $AltM = trim(Filter_Input(INPUT_POST, 'AltM'));
   if ($AltM == '') {$AltM = 0.0;}
   $AltM = FloatVal($AltM);
   $AltM = (($AltM < 0)? '-':'+').abs($AltM);

// ------------------------------------
// Store interface arguments in cookie.

   $CookieDataString = "$LocName|$TimeZone|$DSSTYN|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$TimeScale|$Longitude|$Latitude|$AltM";
   SetCookie ($CookieName, $CookieDataString, $ExpiresIn30Days);
}

// -----------------------------------------------------------------------------
// Construct proper date and time string to be used with NASA/JPL Horizons API.
// After this point, errors should be caught and reported by the Horizons API.

   $StartDate = "$StartBCAD $StartYear $StartMonth $StartDay";

// -------------------
// Set for UTC or UT1.

   $xTimeScale = '';

   if ($TimeScale == 'UT')
      {
       $xTimeScale .= ($StartBCAD == 'AD' and $StartYear > 1961)? '1':'C';
      }

// ###########################################################################
// BEGIN MAIN COMPUTATIONS CODE STARTING HERE AND PUT RESULTS INTO TEXT AREAS.
// THE @ SYMBOLS SUPPRESS THE WARNING MESSAGES CAUSED BY SOME INTERNAL ERRORS.


// --------------------------------------------
// Generate the raw topocentric ephemeris data.
// This raw data is not displayed but can be if
// you wish to see its content.   The same con-
// tent is reformatted for easier printing.

   $RawEphemerisData = Moon_Phase($StartDate,$StartTime,$TimeScale,$TimeZone,
                                  $DSSTYN,$LonDeg,$LatDeg,$AltM);

// ------------------------------------------
// Compute moon rise/transit/set times table.

   $RTSTimesTable = RTS_Times_Table ('301',$StartDate,$TimeScale,$TimeZone,
                                     $DSSTYN,$LonDeg,$LatDeg,$AltM);

// ------------------------------------------------------------
// Return empty string if no ephemeris statistics are returned.

   if ($RawEphemerisData == $RTSTimesTable)  {$RTSTimesTable = '';}

// --------------------------------------------------------
// Extract the ephemeris work table for subsequent parsing.
// It is possible that an error or other message may be re-
// turned instead.

   $TopoEphemWorkData = Extract_Ephem_Data($RawEphemerisData);
   $EphemStatsData    = Ephem_Stats_Table($RawEphemerisData);

// ---------------------------
// Patch to catch date errors.
   $EphemStatsData = Str_Replace('Cannot interpret date. Type "?!" or try YYYY-MMM-DD {HH:MN} format.','ERROR: Invalid calendar date.', $EphemStatsData);



// --------------------------------------
// Construct output TextArea1 text block.

   $TextArea1Text =
"======================================================
         TOPOCENTRIC LUNAR PHASE CALCULATOR FOR
           ANY GIVEN DATE, TIME AND LOCATION

Calendar Date/Time
$StartBCAD $StartYear-$StartMonth-$StartDay  $StartTime
Time Scale: $xTimeScale
Time Zone UT$TimeZone (Ignored if TT Scale)
$DSSTMssg

Location Name
$LocName

Longitude  = $Longitude&deg;
Latitude   = $Latitude&deg;
Altitude   = $AltM m

Reference Frame ICRF
International Celestial Reference Frame

Mean refraction is applied to rise/set computations.

$RTSTimesTable
$EphemStatsData";




// --------------------------------------
// Construct output TextArea2 text block.

   $TextArea2Text = '';





// ---------------------------------------------------------------------------
// Determine number of text columns and rows to use in the output text areas.
// These valuse will vary randomly according to the text block width and length.
// This way you do not have to keep rewriting code to update the changing sizes
// of the text areas manually.

   $Text1Cols = 1 + Max(Array_Map('StrLen', PReg_Split("[\n]", trim($TextArea1Text))));
   $Text1Rows = 2 + Substr_Count($TextArea1Text, "\n");
   if ($Text1Cols < 81) {$Text1Cols = 55;}

   $Text2Cols = 1 + Max(Array_Map('StrLen', PReg_Split("[\n]", trim($TextArea2Text))));
   $Text2Rows = 2 + Substr_Count($TextArea2Text, "\n");
   if ($Text2Cols < 91) {$Text2Cols = 80;}

// --------------------------------------
// Round radial velocities to 3 decimals.

   $RadVkph  = SPrintF("%+1.3f", $RadVkph);
   $RadVmiph = SPrintF("%+1.3f", $RadVmiph);

// --------------------------------------------------
// Generate client web page to display the ephemeris.

   print <<< _HTML

<!DOCTYPE HTML>
<HTML lang='en-us'>

<head>

<title>$_BROWSER_TAB_TEXT_</title>

<meta name='viewport' content='width=device-width, initial-scale=0.8'>

<meta http-equiv='content-type' content='text/html; charset=UTF-8'>
<meta http-equiv='pragma'  content='no-cache'>
<meta http-equiv='expires' content='-1'>
<meta name='description' content='JPL, NASA,Horizons,current moon phase, current lunar phase'>
<meta name='keywords' content='PHPScienceLabs.com'>
<meta name='author' content='Jay Tanner - https://www.PHPScienceLabs.com'>
<meta name='robots' content='noindex,nofollow'>
<meta name='googlebot' content='noindex,nofollow'>

<style type='text/css'>
 BODY {color:white; background:#000020; font-family:Verdana; font-size:12pt; line-height:125%;}

 TABLE
{font-size:13pt; background:#000020; border: 1px solid black;}


 TD
{
 color:black; background:white; line-height:150%; font-size:10pt;
 padding:6px; text-align:center;
}

 UL
{font-family:Verdana; font-size:12pt; line-height:150%; text-align:justify;}


 PRE
{
 background:white; color:black; font-family:monospace; font-size:12.5pt;
 font-weight:bold; text-align:left; line-height:125%; padding:6px;
 border:2px solid black; border-radius:8px;
 page-break-before:page;
}


 DIV
{
 font-family:Verdana; font-size:11pt;  font-weight:normal;
 line-height:125%; padding:6px;
}


 TEXTAREA
{
 background:black; color:silver; font-family:monospace; font-size:11pt;
 font-weight:bold; padding:4pt; white-space:pre; border-radius:8px;
 line-height:125%;
}



 INPUT[type='text']::-ms-clear {width:0; height:0;}

 INPUT[type='text']
{
 font-family:monospace; color:black; background:white; font-size:12pt;
 font-weight:bold; text-align:center; box-shadow:2px 2px 3px #666666;
 border:2px solid black; border-radius:4px;
}
 INPUT[type='text']:focus
{
 font-family:monospace; background:white; box-shadow:2px 2px 3px #666666;
 font-size:12pt; border:2px solid blue; text-align:center; font-weight:bold;
 border-radius:4px;
}



 INPUT[type='submit']
{
 background:black; color:#777777; font-family:Verdana; font-size:12pt;
 font-weight:bold; border-radius:4px; border:4px solid #777777;
 padding:3pt;
}
 INPUT[type='submit']:hover
{
 background:black; color:white; font-family:Verdana; font-size:12pt;
 font-weight:bold; border-radius:4px; border:4px solid GreenYellow;
 padding:3pt;
}



// ----------------------------------------
// Link states MUST be set in the following
// sequential order to work properly:
// :link, :visited, :hover, :active

 A:link
{
 font-size:10pt; background:transparent; color:DodgerBlue; border-radius:4px;
 font-family:Verdana; font-weight:bold; text-decoration:none;
 line-height:175%; padding:3px; border:1px solid transparent;
}

 A:visited
{
 font-size:10pt; background:transparent; color:blue; border-radius:4px;
}

 A:hover
{
 font-size:10pt; background:yellow; color:black; border:1px solid black;
 box-shadow:1px 1px 3px #222222; border-radius:4px;
}

 A:active
{
 font-size:10pt; background:yellow; color:black; border-radius:4px;
}




 HR {background:red; height:4px; border:0px;}



[title-text]:hover:after
{
 opacity:1.0;
 transition:all 1.0s ease 1.0s;
 text-align:left;
 visibility:visible;
}

[title-text]:after
{
 opacity:1.0;
 content:attr(title-text);
 text-align:left;
 left:50%;
 background-color:yellow;
 color:black;
 font-size:10pt;
 position:absolute;
 padding:1px 5px 2px 5px;
 white-space:pre;
 border:1px solid red;
 z-index:1;
 visibility:hidden;
}

[title-text] {position: relative;}



::selection{background-color:yellow !important; color:black !important;}
::-moz-selection{background-color:yellow !important; color:black !important;}
</style>

</head>

<body>

<form name='form1' method='post' action="">

<table width='700' align='top' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="99" style='color:white; background-color:#000066; border:2px solid white; border-radius:8px 8px 0px 0px;'>$_INTERFACE_TITLE_</td>
</tr>
</table>




<table width='700' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr title-text=' This can be any optional location name or lable text. '>
<td style='text-align:center; background:LightCyan; line-height:175%;'><b>Optional&nbsp;Location&nbsp;Name</b><br><input name='LocName'  type='text' value="$LocName"  size='56' maxlength='55'></td>
</tr>
</table>



<!-- Geographic (GPS) Coordinates --->
<table width='700' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td style='text-align:center; background:#CFFFCF; line-height:300%;'><div title-text=' Coordinates can be entered as decimal degrees or \n as Deg Min Sec separated by spaces. \n\n Make sure Geographic Coordinates are within the \n given Time Zone or the Rise/Set times may not be \n accurate. \n\n For time zones and longitudes, +Positive = East'><b>Geographic&nbsp;GPS&nbsp;Coordinates</b></div>
Longitude <input name='Longitude'  type='text' value="$Longitude"  size='15' maxlength='14' title=' Decimal Degrees  or  Deg Min Sec  Separated by Spaces\n +Positive = East '>&nbsp;&nbsp;&nbsp;&nbsp;
Latitude <input name='Latitude'  type='text' value="$Latitude"  size='15' maxlength='14' title=' Decimal Degrees  or  Deg Min Sec  Separated by Spaces\n Negative = South '>&nbsp;&nbsp;&nbsp;&nbsp;
Altitude <input name='AltM'  type='text' value="$AltM"  size='8' maxlength='9' title=' Elevation in Meters Relative to Sea Level '> m
</td>
</tr>
</table>



<!-- Time Zone Offset From UT --->
<table width='700' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan='3' style='text-align:center; background:LightYellow;'>Base&nbsp;Time&nbsp;Scale
<input name='TimeScale'  type='text' value="$TimeScale"  size='3'  maxlength='2' title=' UT = Universal Time Scale (Default) \n Uses the Time Zone setting. \n\n TT = Terrestrial (Barycentric Dynamical) Time. \n Ignores the Time Zone and Daylight/Summer \n Time settings. '>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Time&nbsp;Zone
<input name='TimeZone'  type='text' value="$TimeZone"  size='7' maxlength='6' title=' Local time zone offset from UT    00:00 = UT \n &plus;Positive = East \n\n NOTE: Time Zone offset is ignored if using TT scale. '>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Daylight&nbsp;/&nbsp;Summer&nbsp;Time&nbsp;<input name='DSTYN'  type='text' value="$DSSTYN"  size='4' maxlength='3' title-text=' Daylight Saving / Summer Time ? \n\n No = Use Standard Time (Default) \n This Daylight/Summer \n Time setting applies ONLY to the  UT  time scale. \n This Daylight/Summer Time setting is IGNORED if using the  TT  time scale. '>
</td>
</tr>
</table>


<!-- Date and Time --->
<table width='700' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr title-text=' ALL BLANK = Current Date/Time '>
<td colspan='3' style='text-align:center; background:white; line-height:175%;'><b>&nbsp;&nbsp; Date and Time</b><br>
<input name='StartBCAD'  type='text' value="$StartBCAD"  size='3'  maxlength='2'><input name='StartYear'  type='text' value="$StartYear"  size='5'  maxlength='4'><input name='StartMonth' type='text' value="$StartMonth" size='4'  maxlength='3'><input name='StartDay'   type='text' value="$StartDay"   size='3'  maxlength='2'>&nbsp;&nbsp;
<input name='StartTime'  type='text' value="$StartTime"  size='13' maxlength='12'>
</td>
</tr>
</table>


<!-- Source Code View and/or Download Link. --->
<table width="700">
<tr>
<td colspan="1" style="font-size:10pt; color:GreenYellow; background:LightCyan; text-align:center;">
<b><a href='View-Source-Code.php' target='_blank' style='text-decoration:none; padding:4px;'>View Source Code</a></b>
<br>
<div style='text-align:center;'>
<b><a href='20000-Year-Lunar-Phase-Calculator.7z' style='font-size:10pt; text-decoration:none; padding:4px;' target='_blank'>Download This Program + Phase Images&nbsp; &nbsp;(Approx.&nbsp; 10 MB)</a></b>
</div>
</td>
</tr>
</table>



<!-- Compute Button --->
<table width='700' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="$ColSpan" style='background:#000020;'>
<input name='ComputeButton' type='submit' value=' C O M P U T E ' OnClick="$_COMPUTING_">
</td>
</tr>
</table>


<!-- Moon phase image to be displayed. --->
<table width='700'>
<tr>
<td colspan="99" style='color:white; background:black; text-align:center; border:1px solid blue;'>
<span style='color:gray; font-size:8pt;'>N</span><br>
<img src='lunar-phases/$PhaseAngImg'><br>
<span style='color:silver; font-size:9pt;'>Phase Angle: &nbsp; $PhaseAngDeg&deg;<br>$PhaseText<br>Distance:&nbsp; $DistKm km &nbsp;=&nbsp; $DistMi mi<br>
</span>
</td>
</tr>
</table>


<table width='790' border='0' align='bottom' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="$ColSpan" style='background:#000020; text-align:center;'>
<div style='color:lime; font-size:10pt;'>Double-Click Within Text Area to Select ALL Text</div>
<textarea name='TextArea1' style='color:white; background:#003300; padding:8px; border:2px solid green;' cols="$Text1Cols" rows="$Text1Rows" ReadOnly OnDblClick='this.select();' OnMouseUp='return true;'>
$TextArea1Text
</textarea></td>
</tr>


<!--
<tr>
<td colspan="$ColSpan" style="background:black; text-align:center;">
<div style='color:lime; font-size:10pt; text-align:left;'>Double-Click Within Text Area to Select ALL Text</div>
<textarea name='TextArea2' style='color:black; background:white; padding:6px;' cols="$Text2Cols" rows="$Text2Rows" ReadOnly OnDblClick='this.select();' OnMouseUp='return true;'>
$TextArea2Text
</textarea><br>
<span style='color:silver; background:black;'>PHP Program by $_AUTHOR_ $_REVISION_DATE_</span>
</td>
</tr>
--->

</table>


</form>

<br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br>

</body>


_HTML;





// ####################################################################
// CUSTOM UTILITY FUNCTIONS CODE CAN START HERE, BEYOND THE HTML ABOVE.


/*
   ============================================================================
   This function converts a time elements (HMS) string to decimal hours.

   The time argument elements can be separated by colons or spaces.
   Examples: 'hh:mm:ss'  or  'hh mm ss'

   Time strings can be entered in several formats.
   Where:
   1 element  =  'Hours'
   2 elements =  'Hours Minutes'
   3 elements =  'Hour Minutes Seconds'


   NO DEPENDENCIES

   ###########################################################################
*/

   function HMS_to_Hours ($hhmmss)
{
// ----------------------------------------
// Set internal working decimals precision.

   $Q = 32;

// --------------------------
// Read time string argument.

   $hms = trim($hhmmss);

// -------------------------------
// Replace any colons with spaces.

   $hms = Str_Replace(':', ' ', $hms);

// ------------------------------------------------
// Normalize the spacing and then split and extract
// the individual time string elements (hh, mm,ss).
// Any arguments beyond the third will be ignored.

   $hms = PReg_Replace("/\s+/", " ", trim($hms));
   $wHMS = PReg_Split("[ ]", $hms);
   $wHMScount = count($wHMS);
   $hh = ($wHMScount >= 1)? bcAdd($wHMS[0],"0", $Q) : "0";
   $mm = ($wHMScount >= 2)? bcAdd($wHMS[1],"0", $Q) : "0";
   $ss = ($wHMScount >= 3)? bcAdd($wHMS[2],"0", $Q) : "0";

// --------------------------------------------------
// Remember and then remove any numerical (-/+) sign.

   $NumSign = (substr($hhmmss,0,1) == '-')? '-' : '';
   $hh = Str_Replace('-', '', $hh);
   $hh = Str_Replace('+', '', $hh);

// --------------------------------------------------------------
// If original angle argument began with a + sign, then preserve
// it so that all returned positive results will have a  + sign.
// Otherwise, positive results will NOT have a + sign.  Negative
// values are not effected.

   if (substr($hhmmss,0,1) == '+') {$NumSign = '+';}

// -----------------------------------------------------------------------
// Compute decimal hours value equivalent to the combined hh,mm,ss values.

   $w2 = bcAdd(bcAdd(bcMul($hh,'3600',$Q),bcMul($mm,'60',$Q),$Q),$ss,$Q);

// ------------------------------------------------------------
// If result equates to zero, then suppress the numerical sign.

   if (bcComp($w2, '0', $Q) == 0) {$NumSign = '';}

// --------------------------------
// Round off result to 16 decimals.

   $w =  bcAdd(bcDiv($w2,3600, $Q), '0.00000000000000005', 16);

// ------------------------------------
// Reattach numerical sign and trim off
// any redundant zeros/decimal point.

// DONE.
   return $NumSign.Rtrim(Rtrim($w, '0'), '.');

} // End of  HMS_to_Hours (hhmmss)




/*
   ###########################################################################
   This function returns the equivalent H:M:S time string with several for-
   matting options.

  hours         = +/- Decimal hours value
  ssDecimals    = Number of decimals in seconds part

  posSign = Symbol to use for positive values ('' or '+')
      ''  = Empty = Return numbers only, no symbols.
      '+' = Attach '+' sign to positive values.
            Has no effect on negative values.

  SymbolsMode   = If or not to attach time symbols (h m s) or (:)
                  'h' = '01h 02m 03s' (Default)
                  ':' = '01:02:03'
                  ''  = '01 02 03'
   ERRORS
   No special  error checking is done by this function.

   NO DEPENDENCIES

   ###########################################################################
*/

   function Hours_to_HMS ($hours, $ssDec=0, $posSignSymb='', $SymbMode='h')
{
// Initialize symbol carriers.
   $_h_ = $_m_ = $_s_ = '';
   if (trim($posSignSymb) == '')  {$posSignSymb = FALSE;}

// Remember original numerical sign and work with absolute value.
   $sign = ($hours < 0)? '-' : '';    $hours = abs($hours);

// Remember numerical sign to be restored on exit, if any.
   if (($posSignSymb === TRUE or $posSignSymb == '+') and $sign == '')
       {$sign = '+';}

// Compute time elements from absolute hours argument.
   $hh = floor($hours);
   $minutes = 60*($hours - $hh);    $mm = floor($minutes);
   $seconds = 60*($minutes - $mm);  $ss = SPrintF("%1.3f",  $seconds);

// Format the time elements.
   $hh = SPrintF("%02d",  $hh);
   $mm = SPrintF("%02d",  $mm);
   $ss = SPrintF("%1.$ssDec" . "f", $ss);

// Patch for that blasted 60s glitch.
   if ($ss == 60) {$ss = 0; $mm++;}
   if ($mm == 60) {$mm = 0; $hh++;}

   $hh = SPrintF("%02d",  $hh);
   $mm = SPrintF("%02d",  $mm);

   $ss = SPrintF("%1.$ssDec" . "f", $ss);
   if ($ss < 10) {$ss = "0$ss";}

// Attach optional (h, m, s, :) symbols as indicated.
// Default = 'h'
   if ($SymbMode == 0 or $SymbMode == '')
      {$_h_  =  $_m_  =  $_s_  = ' ';}

   if ($SymbMode == 1 or $SymbMode == ':')
      {$_h_  =  ':';  $_m_  =  ':';  $_s_  = '';}

   if ($SymbMode == 2 or StrToLower($SymbMode) == 'h')
      {$_h_  =  'h ';  $_m_  =  'm ';  $_s_  =  's';}

   $w = "$sign$hh$_h_$mm$_m_$ss$_s_";

// Done.
   return $w;

} // End of  Hours_to_HMS(...)






/*
   ###########################################################################
   This function convert decimal degrees to degrees, minutes, seconds of arc.
   The DMS angular elements are returned in a space-delimited string.

   INPUT:
   AngDeg = Degrees as numerical string, like '123.3456789065741'

   Optional symbols may be attached to the angle elements.  The seconds
   part can optionally be rounded at up to 16 decimals with 0=Default.
   +009° 10' 11.1234567890123456"
   -017° 22' 13.184"

   posSign = '+' = Force positive sign in positive output values, such
             as +085 23 18.713  instead of  085 23 18.713
             '' = Empty string = Default = No positive sign attached.
             This has no effect on negative values.

   ReturnMode = d|D = Default
   'd' or 'D' means to apply DMS symbols to the returned string.
   123° 04' 56.7891"
  -000° 00' 01.234"

   ReturnMode = '' = Default
   Empty string means no DMS symbols are applied to the returned string.
   123 04 05.6789
  -015 01 09

   OUTPUT:
   Returns a space-delimited string like:
   12° 34' 56.789"

   ERRORS:
   This function does NOT check for errors.

   NO DEPENDENCIES
   ###########################################################################
*/

   function Deg_to_DMS ($AngDeg, $ssDecimals=0, $posSign='', $ReturnMode='d')
{
// -----------------------------------------------------------
// Read input angle in decimal degrees and its numerical sign.
// Work with absolute value.  Sign will be restored on exit.

   $a = trim($AngDeg);   $sign = ($a < 0)? '-' : '';
   $a = Str_Replace('+', '', Str_Replace('-', '', $a));

// ---------------------------------
// Account for numerical sign output.

   if (($posSign === TRUE or $posSign == '+') and $sign == '') {$sign = '+';}

// ----------------------------------------------------
// Set number of decimals to use in seconds part not to
// exceed 16 decimals.  Default = 0 decimals.

   $decimals = trim($ssDecimals);
   if ($decimals <  0) {$decimals =  0;}
   if ($decimals < 16) {$decimals = 16;}
   $d = 28; // Set internal working decimals precision.

// --------------------------------------------
// Compute degrees, minutes and seconds of arc.

   $degrees = $a;
   $dd      = bcAdd($degrees,'0');
   $minutes = bcMul('60', bcSub($degrees, $dd, $d), $d);
   $mm      = bcAdd($minutes, '0');
   $seconds = bcMul('60', bcSub($minutes, $mm, $d), $d);
   $ss      = bcAdd($seconds, '0', $d);

// ----------------------------------
// Patch for that blasted 60s glitch.

   if ($ss == 60) {$ss = 0; $mm++;}
   if ($mm == 60) {$mm = 0; $dd++;}

// ----------------------------
// Format the angular elements.

   $dd = SPrintF("% 3d",  $dd);
   $mm = SPrintF("%02d",  $mm);
   $ss = bcAdd($ss, '0.'.Str_Repeat('0', $ssDecimals).'5', $ssDecimals);
   if ($ss < 10) {$ss = "0$ss";}

// ------------------------------------------
// Attach optional DMS symbols, if indicated.
// Default = d|D  = Attach DMS symbols.
// Empty string   = No DMS symbols attached.

   $_deg_ = $_mm_ = $_ss_ = '';

   if ($ReturnMode === TRUE or substr(StrToLower($ReturnMode),0,1) == 'd')
      {
       $_deg_ = "&deg;";     $_mm_ = "'";     $_ss_ = '"';
      }

// Do some output formatting.
   $w = "$sign$dd$_deg_ $mm$_mm_ $ss$_ss_";
   $w = Str_Replace('-  ', '  -', $w);
   $w = Str_Replace('- ',  ' -',  $w);
   $w = Str_Replace('+  ', '  +', $w);
   $w = Str_Replace('+ ',  ' +',  $w);

// -----
// Done.
   return $w;

} // End of  Deg_to_DMS(...)










/*
   ###########################################################################
   This function converts a DMS time string into equivalent decimal degrees.

   INPUT: 1, 2 or 3 DMS angle elements string as 'Deg Min Sec' values.
   1 value  = Deg.dddd
   2 values = Deg Min
   3 values = Deg Min Sec

   Any extra elements beyond the 3 'Deg Min Sec' vaules will be ignored.

   Example: Given DMS string = '9 12 50.306'
            Returned = 9.2139738888888889

   NO DEPENDENCIES

   ###########################################################################
*/

   function DMS_to_Deg ($DMSstr)
{
   $Q = 32;

// ---------------------------------------------------------
// Read angular arguments string ('Deg Min Sec') separated
// by spaces.

   $dms = trim($DMSstr);

// ----------------------------------------------------
// Normalize the spacing and then split and extract the
// individual angular string elements (dd, mm,ss).

   $dms = PReg_Replace("/\s+/", " ", trim($dms));
   $wdms = PReg_Split("[ ]", $dms);
   $wdmscount = count($wdms);
   $dd = ($wdmscount >= 1)? bcAdd($wdms[0],"0", $Q) : "0"; // Deg
   $mm = ($wdmscount >= 2)? bcAdd($wdms[1],"0", $Q) : "0"; // Min
   $ss = ($wdmscount >= 3)? bcAdd($wdms[2],"0", $Q) : "0"; // Sec

// ------------------------------------------------
// Remember and then remove any numerical (±) sign.

   $NumSign = (substr($DMSstr,0,1) == '-')? '-' : '';
   $dd = Str_Replace('-', '', $dd);
   $dd = Str_Replace('+', '', $dd);

// -------------------------------------------------------------
// If original angle argument began with a + sign, then preserve
// it so that all returned positive results will have a + sign.
// Otherwise, positive results will NOT have a + sign.

   if (substr($DMSstr,0,1) == '+') {$NumSign = '+';}

// ----------------------------------------------
// Compute decimal degrees/hours value equivalent
// to the given dhms argument elements.

   $w2 = bcAdd(bcAdd(bcMul($dd,"3600",$Q),bcMul($mm,"60",$Q),$Q),$ss,$Q);

// -----------------------------------------------------------
// If result equates to zero, then suppress any numerical sign.

   if (bcComp($w2, "0", $Q) == 0) {$NumSign = '';}

// ---------------------------------------------------------
// round off result to 16 decimals, recalling original sign.

   return $NumSign . bcAdd(bcDiv($w2,"3600",$Q), "0.00000000000000005",16);

} // End of  DMS_to_Deg (...)





/*
   ###########################################################################
   This function returns the 3-letter month abbreviation for any given month
   number from 1 to 12. If the month number is outside of this range, 'Xxx'
   is returned as an error.

   The input argument may also be a 3-letter month abbreviation.  If the
   3-letter abbreviation is invalid, 'Xxx' will be returned as an error.

   ###########################################################################
*/
   function Month_to_Str ($Month='')
{
   $Mmm = $m = UCFirst(substr(StrToLower(trim($Month)),0,3));
   $MONTHS = 'JanFebMarAprMayJunJulAugSepOctNovDec';

// ----------------------------------------------------------------------
// Do this if month argument is text string ('Sun','Mon','Tue', ...) etc.

   if (!Is_Numeric($Mmm))
      {
       $i = StrPos($MONTHS, UCFirst(substr(StrToLower(trim($Month)),0,3)));

       if ($i === FALSE) {return 'Xxx';}

       $m = 1 + $i/3;
      }

   else

// -------------------------------------
// Do this if month argument is numeric.
 {
   $m = abs($m);  if ($m < 1 or $m > 12) {return 'Xxx';}
 }
   return substr($MONTHS, 3*($m-1), 3);
}








/*
   ###########################################################################
   Given a raw ephemeris table, this function will return the isolated table
   between the ephemeris starting and ending markers ($$SOE and $$EOE).

   It is possible that the returned text may not contain an ephemeris table
   if other data was requested or it may be an error or other message. This
   possibility needs to be considered before attempting to parse the table.

   Marker  Meaning
   ------  ------------------
   $$SOE   Start-Of-Ephemeris
   $$EOE   End-Of-Ephemeris

   ERRORS:
   A message will be returned if no ephemeris start/end markers are found,
   however it may not always represent an error.

   ###########################################################################
*/
   function Extract_Ephem_Data ($RawEphemeris)
{
   $T = trim($RawEphemeris);

// ---------------------------------------------
// ERROR if no ephemeris table found. Otherwise,
// return the isolated ephemeris table.

   $i = StrPos($T, '$$SOE');
        if ($i == FALSE)
           {
            return 'STATUS: No Horizons ephemeris table found.';
           }

// ------------------------------------------
// Since a start marker was found, we now set
// a pointer to the corresponding end marker.

   $j = StrPos($T, '$$EOE');

// -----------------------------------------
// Extract only the isolated ephemeris table
// between the pointers exclusive. All other
// text will be left behind.

   $EphemTable = substr($T, $i+5, $j-$i-5);

   return $EphemTable;
}






/*
   This function returns a table of Rising, Transit and Setting times
   for most solar system bodies for which a topocenrtric ephemeris is
   possible.

   ARGUMENTS:
   BodyID    = NASA/JPL BodyID (e.g. 301=Moon; 10=Sun; 499=Mars; 299=Mercury)
   CalDate   = Calendar date in  'BC|AD Yyyy-Mmm-dd'  format
   TimeScale = 'UT' or 'TT'
   TimeZone  = Time Zone Offset From UT (Negative = West)
   DSSTYN    = Daylight Saving/Summer Time (Yes/No)
   LonDeg    = Longitude in degrees (Negative = West)
   LatDeg    = Latitude in degrees  (Negative = South)
   AltM      = Altitude in meters relative to sea level = 0

*/

   function RTS_Times_Table ($BodyID,$CalDate,$TimeScale,$TimeZone,
                             $DSSTYN,$LonDeg,$LatDeg,$AltM)
{
   GLOBAL $DSSTMssg;

// ------------------------------------
// URL-encode the calendar date string.

   $CalDate = URLEncode($CalDate);

// ------------------------
// Read Horizons API query.

   $BodyID = trim($BodyID);

// -----------------------------------------------
// Convert meters to km for internal computations.

   $AltKm = trim($AltM)/1000;

// -----------------------------------------------
// Compute Time Zone in decimal hours. This is for
// taking Daylight Saving/Summer Time into account.
// The argument MUST first be specified in  +-hh:mm
// format and a numerical sign MUST be the first
// character in the TimeZone argument string.
// Valid examples: -05:00  and  +09:30, etc.

   $DSSTYN  = StrToUpper(substr($DSSTYN,0,1));
   $DSSTAdj = ($DSSTYN == 'Y')? 1:0;

   $w = trim($TimeZone);
   $TZHours = IntVal(substr($w,0,1).substr($w,1,2))+IntVal(substr($w,4,2))/60;
   $TZHours += trim($DSSTAdj);

// ------------------------------------------------------
// Read Body ID argument and URL-encode it, just in case.

   $command = URLEncode($BodyID);

// =========================================================
   $From_NASA_JPL_Horizons_API =
   "https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   "&COMMAND='$command'"                                   .
   "&OBJ_DATA='YES'"                                       .
   "&MAKE_EPHEM='YES'"                                     .
   "&EPHEM_TYPE='OBSERVER'"                                .
   "&CAL_FORMAT='BOTH'"                                    .
   "&ANG_FORMAT='DEG'"                                     .
   "&APPARENT='REFRACTED'"                                 .
   "&CENTER='COORD@399'"                                   .
   "&COORD_TYPE='GEODETIC'"                                .
   "&SITE_COORD='$LonDeg, $LatDeg, $AltKm'"                .
   "&TIME_DIGITS='MINUTES'"                                .
   "&TIME_ZONE='$TZHours'"                                 .
   "&START_TIME='$CalDate 00:00:00 $TimeScale'"            .
   "&STOP_TIME='$CalDate 23:59:59'"                        .
   "&STEP_SIZE='1 minute'"                                 .
   "&EXTRA_PREC='YES'"                                     .
   "&RANGE_UNITS='AU'"                                     .
   "&SUPPRESS_RANGE_RATE='YES'"                            .
   "&QUANTITIES='4,42"                                     .
   "&CSV_FORMAT='YES'"                                     ;
// =========================================================

// ------------------------------------------------------------------
// Send query to Horizons API to get requested data in plain-text
// format.  All ephemerides are returned in plain-text CSV format
// to facilitate easy column parsing.

   $RawResponse = trim(File_Get_Contents($From_NASA_JPL_Horizons_API));

/* ==================================================================
   At this point we now have the raw ephemeris as long as there were
   no erroneous arguments.   However, not everything returned by the
   program is an ephemeris. Data lookups are also possible using any
   of several special search commands or directives. However, this
   function was aimed primarily at solar system body ephemerides and
   making customized astronomical almanacs.

   When attempting to parse the returned data, it is first necessary
   to determine what kind of data was returned or it could lead to a
   program crash if not parsed correctly according to its format.

   Any returned ephemeris will be contained between 2 markers:
   $$SOE = Start Of Ephemeris
   $$EOE = End Of Ephemeris

   An ephemeris table can consist of one or multiple lines of data,
   but it will always be in between the $$SOE and $$EOE markers.

   If there is no starting marker ($$SOE), then the returned response
   is NOT an ephemeris and could possibly be  an error message due to
   an invalid API calling argument.

   The raw response  should be scanned for  error message strings and
   filtered accordingly.
   ==================================================================
*/

// ----------------------------------------------------------------
// If there is no ephemeris table start pointer, then no ephemeris
// was returned, so return (RawResponse) as-is.

   $i = StrPos($RawResponse, '$$SOE');
        if ($i === FALSE) {return $RawResponse;}
   $j = StrPos($RawResponse, '$$EOE');

// --------------------------------------------------------------
// If an ephemeris table is detected, then isolate and return it.

   $EphemTable = substr($RawResponse, $i+5, $j-$i-5);

   $EphemTable = Str_Replace(",\n", "\n", $EphemTable);

// --------------------------
// Store table in work array.

   $wTable = '';
   $wArray = Preg_Split("[\n]", trim($EphemTable));
   $wCount = count($wArray);

   for ($i=0;   $i < $wCount;   $i++)
       {
        $CurrLine = trim($wArray[$i]);
        list($DateTimeStr,$JDateUT) = PReg_Split("[,]", $CurrLine);

        if (Is_Numeric(substr($CurrLine,0,1)))
           {$xLine = "AD $CurrLine";}
        else
           {
            $xLine = "BC " . substr($CurrLine, 1, StrLen($CurrLine));
           }

        $wTable .= "$xLine\n";
       }

// --------------------------------------
// Copy R/T/S work table into work array.

   unset($wArray);

   $wArray = PReg_Split("[\n]", trim($wTable));
   $wCount = count($wArray);

// ---------------
// Seek RTS times.

   $RTSTable = '';

   for ($i=1;   $i < $wCount;   $i++)
  {

     $PrevLine = $wArray[$i-1];

     list
    (
     $PrevDateTimeStr,
     $PrevJDate,
     $w,$w,
     $PrevAzimDeg,
     $PrevElevDeg,
     $PrevLHAHrs
    ) = PReg_Split("[,]", $PrevLine);

     $PrevDateTimeStr = $PrevDateTimeStr;
     $PrevJDate       = trim($PrevJDate);
     $PrevAzimDeg     = trim($PrevAzimDeg);
     $PrevElevDeg     = trim($PrevElevDeg);
     $PrevLHAHrs      = trim($PrevLHAHrs);

     $CurrLine = $wArray[$i];

   list
  (
   $CurrDateTimeStr,
   $CurrJDate,
   $w,$w,
   $CurrAzimDeg,
   $CurrElevDeg,
   $CurrLHAHrs
  ) = PReg_Split("[,]", $CurrLine);

   $CurrDateTimeStr = $CurrDateTimeStr;
   $CurrJDate       = trim($CurrJDate);
   $CurrAzimDeg     = trim($CurrAzimDeg);
   $CurrElevDeg     = trim($CurrElevDeg);
   $CurrLHAHrs      = trim($CurrLHAHrs);

// ----------------------------------
// ISOLATE THE LINES CONTAINING THE
// R/T/S TIMES TO THE NEAREST MINUTE.

// -----------------
// Seek rising time.
   if ($PrevElevDeg < 0 and trim($CurrElevDeg) > 0)
      {
       $CurrElevDeg = SPrintF("%5.2f", trim($CurrElevDeg));
       $CurrAzimDir = Str_Replace('E  ', ' E ', Compass_Symbol($CurrAzimDeg));
       $CurrAzimDeg = SprintF("%6.2f", $CurrAzimDeg);
       $RTSTable   .= substr($CurrDateTimeStr,0,20) . ", $CurrAzimDeg&deg;,  $CurrAzimDir, Rise\n";
      }

// ------------------
// Seek transit time.
   if ($PrevLHAHrs  < 0 and $CurrLHAHrs  > 0)
      {
       $CurrElevDeg = SPrintF("%5.2f", $CurrElevDeg);
       $RTSTable .= substr($CurrDateTimeStr,0,20) . ", On Merid   | , Transit, $CurrElevDeg&deg;\n";
      }

// ------------------
// Seek setting time.
   if ($PrevElevDeg > 0 and $CurrElevDeg < 0)
      {
       $CurrAzimDir = Str_Replace('W  ', ' W ', Compass_Symbol($CurrAzimDeg));
       $CurrAzimDeg = SprintF("%6.2f", $CurrAzimDeg);
       $RTSTable .= substr($CurrDateTimeStr,0,20) . ", $CurrAzimDeg&deg;,  $CurrAzimDir, Set\n";
      }
  }

// --------------------------------------------
// Daylight Saving / Summer Time mode messsage.

   if (StrToUpper(substr($DSSTYN,0,1)) == 'Y')
      {$DSSTMssg = 'Daylight Saving / Summer Time';}
   else
      {$DSSTMssg = 'Standard Time';}

   if ($TimeScale == 'TT'){$DSSTMssg = 'TT Time Scale';}

   $w =
"######################################################
            MOON RISE / TRANSIT / SET TIMES
            $DSSTMssg

Local Date and Time     Azim    Dir   Event    Elev
--------------------  --------  ---  -------  ------
$RTSTable
######################################################";

   return trim(Str_Replace(',', ' ', $w));

} // End of  RTS_Times_Table (...)





/*
   ###########################################################################
   This function returns the closest compass symbol corresponding to azimuth
   angle in degrees.  Measured clockwise from North = 0.

   The compass is divided into 16 named
   cardinal zones.

  --------------------------------------
    AZIMUTH ANGLE CLOCKWISE FROM NORTH

  Symbol     direction        Angle Deg.
  ------  ---------------     ----------
    N     North                  0.0
    NNE   North Northeast       22.5
    NE    Northeast             45.0
    ENE   East Northeast        67.5
    E     East                  90.0
    ESE   East Southeast       112.5
    SE    Southeast            135.0
    SSE   South Southeast      157.5
    S     South                180.0
    SSW   South Southwest      202.5
    SW    Southwest            225.0
    WSW   West Southwest       247.5
    W     West                 270.0
    WNW   West Northwest       292.5
    NW    Northwest            315.0
    NNW   North Northwest      337.5
    N     North                360.0

   NO DEPENDENCIES

   ###########################################################################
*/

   function Compass_Symbol ($AzimDeg)
{
   $a = FloatVal($AzimDeg);

   $a -= 360*floor($a/360);

   $i = ($a < 11.25 or $a > 348.75)? 0 : floor(0.5 + $a/22.5);

   $w = trim(substr('N  NNENE ENEE  ESESE SSES  SSWSW WSWW  WNWNW NNWN', 3*$i, 3));

   return substr("$w   ",0,3);

} // End of  Compass_Symbol (...)


/*
   ###########################################################################
   This function constructs the ephemeris stats table from the raw ephemeris.

   ###########################################################################
*/

   function Ephem_Stats_Table ($RawEphemeris)
{
// ---------------------------------------
// Declare some special GLOBAL variables.
// Customized especially for this program.

   GLOBAL $xTimeScale, $TZHours, $TimeZone, $DSSTMssg;
   GLOBAL $PhaseAngDeg, $PhaseAngImg, $PhaseText;
   GLOBAL $DistKm, $DistMi, $DistAU, $TimeScale;
   GLOBAL $RadVkph, $RadVmiph, $RadVmssg;

   $T = trim($RawEphemeris);

// ----------------------------------
// Check for ephemeris. If none, then
// return text as-is without parsing.

   $i = StrPos($T, '$$SOE');  if ($i === FALSE) {return $T;}
   $j = StrPos($T, '$$EOE');

// ---------------------------------------
// Get isolated ephemeris statistics line.
// There should be only a single line.

   $w = trim(substr($T, $i+5, $j-$i-5));

// ------------------------------------------------------------
// Modify beginning of stats line by putting BC|AD era in date.

   if (Is_Numeric(substr($w,0,1)))
       {$w = 'AD ' . $w;}
   if (substr($w,0,1) == 'b')
       {$w = 'BC ' . substr($w, 1,StrLen($w));}

// ---------------------------------------------------------
// AT THIS POINT, WE HAVE THE MODIFIED EPHEMERIS STATS LINE.


// Now, we parse the line and get the individual statistics into
// their respective variables.

   list
      (
       $DateTimeStr,
       $JDXT,
       $u1,
       $u2,
       $RADeg,
       $DeclDeg,
       $DistAU,
       $RadVkms,
       $AngDiamArcSec,
       $AppMag,
       $u3,
       $AzimDeg,
       $ElevDeg,
       $LHAHrs,
       $SidTimeHrs,
       $DeltaTSec,
       $SOTAng,
       $LorT
      ) = PReg_Split("[,]", $w);

// --------------------------------------
// Construct output ephemeris statistics.

   $DateTimeStr = trim($DateTimeStr);
   list($BCAD,$DateStr,$TimeStr) = PReg_Split("[ ]", $DateTimeStr);
   $DateStr = "$BCAD $DateStr";

   $JDXT  = trim($JDXT);
   $u1    = trim($u1);
   $u2    = trim($u2);

   $RADeg = trim($RADeg);
   $RADMS = trim(Deg_To_DMS($RADeg, 3, '', 'd'));
   $RAHrs = $RADeg / 15;
   $RAHMS = Hours_to_HMS($RAHrs, 3, '', 'h');

   $DeclDeg  = trim($DeclDeg);
   $Declsign = ($DeclDeg < 0)? '-' : '+';
   $DeclDeg  = abs($DeclDeg);
   $DeclDMS  = $Declsign.trim(Deg_To_DMS($DeclDeg, 3, '', 'd'));
   $DeclDeg  = $Declsign.$DeclDeg;

   $DistAU = trim($DistAU);
   $DistKm = $DistAU * 149597870.7;
   $DistMi = $DistKm / 1.609344;
   $DistKm = Number_Format($DistKm, 1);
   $DistMi = Number_Format($DistMi, 1);

   $RadVkms  = trim($RadVkms);
   $RadVsign = ($RadVkms < 0)? '-' : '+';
   $RadVkms  = abs($RadVkms);
   $RadVkph  = 3600*$RadVkms;
   $RadVmips = round($RadVkms / 1.609344, 7);
   $RadVmiph = round(3600 * $RadVkms / 1.609344, 7);
   $RadVmips = $RadVsign.$RadVmips;
   $RadVmiph = $RadVsign.$RadVmiph;
   $RadVkms  = $RadVsign.$RadVkms;
   $RadVkph  = $RadVsign.$RadVkph;
   $RadVmssg = ($RadVkms < 0)? 'Approaching Observer' : 'Receding From Observer';

   $AngDiamArcSec = trim($AngDiamArcSec);
   $AngDiamDeg    = $AngDiamArcSec / 3600;
   $AngDiamDMS    = trim(Deg_to_DMS($AngDiamDeg, 3, '', 'd'));
   $AngDiamDeg    = round($AngDiamDeg, 10);

   $AppMag   = trim($AppMag);
   $u3       = trim($u3);
   $AzimDeg  = round(trim($AzimDeg),2);

   $ElevDeg  = round(trim($ElevDeg),2);
   $Elevsign = ($ElevDeg < 0)? '-' : '+';
   $ElevDeg  = abs($ElevDeg);
   $ElevMssg = (($Elevsign == '-')? 'Below':'Above').' Local Horizon';

   $LHAHrs  = trim($LHAHrs);
   $LHAsign = ($LHAHrs < 0)? '-' : '+';
   $LHAHrs  = abs($LHAHrs);
   $LHAHMS  = Hours_to_HMS($LHAHrs, 3, '', 'h');
   $LHADeg  = $LHAHrs * 15;
   $LHADMS  = trim(Deg_to_DMS($LHADeg, 3, '', 'd'));

   $SidTimeHrs = trim($SidTimeHrs);
   $SidTimeHMS = Hours_to_HMS($SidTimeHrs, 3, '', 'h');
   $SidTimeDeg = $SidTimeHrs * 15;
   $SidTimeDMS = trim(Deg_to_DMS($SidTimeDeg, 3, '', 'd'));

   $DeltaTSec = trim($DeltaTSec);
   $NumSign = ($DeltaTSec < 0)? '-':'+';  $DeltaTSec = abs($DeltaTSec);
   $d = 'd';   $h = 'h';   $m = 'm';   $s = 's';
   $days      = $DeltaTSec / 86400;
   $dd        = floor($days);
   $hours     = 24*($days - $dd);
   $hh        = floor($hours);
   $min       = 60*($hours - $hh);
   $mm        = floor($min);
   $sec       = 60*($min - $mm);
   $hh        = SPrintF("%02d", $hh);
   $mm        = SPrintF("%02d", $mm);
   $ss        = SPrintF("%09.6f", $sec);
   $DTHMS     = "$NumSign$dd$d  $hh$h $mm$m $ss$s";
   $DThms     = "$NumSign$dd  $hh:$mm:$ss";
   $DeltaTSec = SPrintF("%+1.6f", $NumSign.$DeltaTSec);


// Determine which time scale symbols to display.
// The TT scale ignores the Time Zone offset.
// Local Zone Time = UT if Time Zone = 00:00
// Local Zone Time = Zone Time if Time Zone <> 00:00 and in UT mode.
   $xTimeScale = $TimeScale;
   if ($TimeScale == 'TT') {$xTimeScale = 'TT';}
   if ($TZHours <> 0 and $TimeScale == 'UT') {$xTimeScale = 'Local Zone Time';}



// --------------------------------------------
// Compute simple phase angle, sans librations.
   $SOTAng    = trim($SOTAng);
   $AzimDir   = Compass_Symbol($AzimDeg);
   $PhaseAngDeg = (trim($LorT) == '/T')? $SOTAng : 360-$SOTAng;
   $PhaseText = Lunar_Phase_to_Text ($PhaseAngDeg);
   $PhaseAngImg = SPrintF("%03d", $PhaseAngDeg).'.png';


// ---------------------------------------
// Construct output ephemeris stats table.

   $OutStats =
"         MOON TOPOCENTRIC EPHEMERIS STATISTICS

Calendar Date       $DateStr
Local Time          $TimeStr
                    $DSSTMssg
Local Time Zone     UT$TimeZone
                    NOTE: Time Zone is ignored if
                          using the TT time Scale

Julian Date        $JDXT $TimeScale

Right Ascension    $RAHMS
                   $RAHrs h
                   $RADMS
                   $RADeg&deg;

Declination        $DeclDMS
                   $DeclDeg&deg;

Distance           $DistKm km
                   $DistMi mi
                   $DistAU AU

Angular Diameter   $AngDiamDMS
                   $AngDiamArcSec&#34;
                   $AngDiamDeg&deg;

Visual Magnitude   $AppMag

Horizon Azimuth    $AzimDeg&deg;\tDirection: $AzimDir
Horizon Elevation  $Elevsign$ElevDeg&deg;\t$ElevMssg

Lunar Phase Angle  $PhaseAngDeg&deg;
$PhaseText

Where:
  0&deg; = New Moon
 45  = Waxing Crescent
 90  = First Quarter
135  = Waxing Gibbous
180  = Full Moon
225  = Waning Gibbous
270  = Last Quarter
315  = Waning Crescent
360  = New Moon

######################################################
Local Hour Angle   $LHAsign$LHAHMS
                   $LHAsign$LHAHrs h
                   $LHAsign$LHADMS
                   $LHAsign$LHADeg&deg;

Loc Sidereal Time   $SidTimeHMS
                    $SidTimeHrs h
                    $SidTimeDMS
                    $SidTimeDeg&deg;

Radial Velocity    $RadVkms km/s
                   $RadVkph km/h
                   $RadVmips mi/s
                   $RadVmiph mi/h
                   $RadVmssg

Delta T (TDB&minus;UT)    $DeltaTSec sec
                    $DTHMS

######################################################
";

   return $OutStats;
}





/*
   ###########################################################################
   This function returns a text description of the lunar phase
   corresponding to the given phase angle argument in degrees.

   The lunar phase angle = 0 to 360 degrees.

   Where:
     0  = New Moon
    45  = Waxing Crescent
    90  = First Quarter
   135  = Waxing Gibbous
   180  = Full Moon
   225  = Waning Gibbous
   270  = Last Quarter
   315  = Waning Crescent
   360  = New Moon

   NO DEPENDENCIES

   ###########################################################################

*/

   function Lunar_Phase_to_Text ($PhaseAngDeg)
{
// round absolute phase angle to nearest integer value.
   $a = trim($PhaseAngDeg);
   if (!Is_Numeric($a) or FloatVal(trim($a)) > 360)
      {return "ERROR: '$a' = Invalid phase angle.  range = 0 to 360 deg.";}

   $a = abs(round(FloatVal($a),0));

   if ($a >= 0  and $a <= 10)
      {return 'New Moon.';}
   if ($a > 10  and $a <= 34)
      {return 'Waxing evening crescent, just past new moon.';}
   if ($a > 34  and $a <= 75)
      {return 'Waxing crescent.';}
   if ($a > 75  and $a <= 88)
      {return 'Waxing crescent, approaching first quarter.';}
   if ($a > 88  and $a <= 101)
      {return 'First quarter.';}
   if ($a > 101 and $a <= 115)
      {return 'Waxing gibbous, just past first quarter.';}
   if ($a > 115 and $a <= 159)
      {return 'Waxing gibbous moon.';}
   if ($a > 159 and $a <= 170)
      {return 'Waxing gibbous, approaching full moon.';}
   if ($a > 170 and $a <= 192)
      {return 'Full Moon.';}
   if ($a > 192 and $a <= 205)
      {return 'Waning gibbous, just past full moon.';}
   if ($a > 205 and $a <= 245)
      {return 'Waning gibbous.';}
   if ($a > 245 and $a <= 260)
      {return 'Waning gibbous, approaching last quarter.';}
   if ($a > 260 and $a <= 271)
      {return 'Last quarter moon.';}
   if ($a > 271 and $a <= 282)
      {return 'Waning crescent, just past last quarter.';}
   if ($a > 282 and $a <= 319)
      {return 'Waning morning crescent.';}
   if ($a > 319 and $a <= 349)
      {return 'Waning morning crescent, approaching new moon.';}
   if ($a > 349 and $a <= 360)
      {return 'New Moon.';}

} // End of  Lunar_ Phase_to_Text (PhaseAngDeg)




/*
   ###########################################################################
   This function computes a basic topocentric ephemeris built around the
   NASA/JPL Horizons API.  It can also retrieve data on any given solar
   system body.

   Computes For UT or TT Scales or Local Zone Time.

   ----------------------------------------
   TOPOCENTRIC EPHEMERIS OUTPUT COORDINATES

   Standard atmospheric refraction is applied.

   Local horizon azimuth is measured clockwise from North = 0

   -----------------------
   LOCAL HORIZON ELEVATION

   Negative = Below the local horizon
   Zero     = The horizon itself
   Positive = Above the local horizon

   ---------------------------------------------------------------------------
   The ephemeris is very basic, tabulating a few general ephemeris values for
   the given body ID which includes the following ephemeris data which can be
   easily edited as desired.

   To alter or customize the ephemeris differently, the QUANTITIES mentioned
   in the function below can be found at the Horizons website at:
   https://ssd.jpl.nasa.gov/horizons/app.html

   Under:
   5 [Edit] Table Settings


   TOPOCENTRIC EPHEMERIS STATS +- OPTIONAL REFRACTION

   Right Ascension                Decimal degrees or HMS
   Declination                    Decimal degrees or DMS
   Distance in AUs                1 AU (Astronomical Unit) = 149597870.7 km
   Angular Diameter               In arcseconds
   Apparent Visual Magnitude      Apparent brightness
   Surface Brightness             Magnitude of 1 sq arcsec of visible disk
   Local Horizon Azimuth          Compass direction   (Clockwise from N=0)
   Local Horizon Elevation        Relative to horizon (Negative  =  Below)
   Local Hour Angle               Neg=Until Meridian;  Pos=Since Meridian
   Local Apparent Sidereal Time   Local meridian star time

   Delta T                        TDB - UT

   S-O-T,/r                       Used to compute the basic lunar phase.
                                  These only appear in a lunar ephemeris
                                  as the final two columns in the table.
                                  Otherwise, Delta T is the final column.

   ###########################################################################
*/

   function Moon_Phase ($StartDate,$StartTime,$TimeScale,$TimeZone,$DSSTYN,
                        $LonDeg,$LatDeg,$AltM)
{
// -----------------------------------------------------------
// Read Horizons API query and make a few special adjustments.

   $TimeScale = (StrToUpper(substr(trim($TimeScale),0,1)) == 'T')? 'TT':'UT';
   $DSSTAdj   = (StrToUpper(substr(trim($DSSTYN),0,1))  == 'N')? 0:1;
   $StartDate = URLEncode($StartDate);

// ------------------------------------------
// Convert meters to km for JPL computations.

   $AltKm = trim($AltM)/1000;

// ------------------------------------------------
// Cancel any DST adjustment if time scale == 'TT'.

   if ($TimeScale == 'TT') {$DSTAdj = 0;}

// ------------------------------------------------
// Compute Time Zone in decimal hours.  This is for
// taking Daylight Saving/Summer Time into account.
// The TimeZone MUST first be specified in  +-hh:mm
// format and a numerical sign MUST be the first
// character in the TimeZone argument string.
// Valid examples: -05:00,  +10:00,  +05:30,  etc..

   $w = trim($TimeZone);
   $TZHours = IntVal(substr($w,0,1).substr($w,1,2))+IntVal(substr($w,4,2))/60;
   $TZHours += $DSSTAdj;

// =========================================================
   $From_NASA_JPL_Horizons_API =
   "https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   "&COMMAND='301'"                                        .
   "&OBJ_DATA='YES'"                                       .
   "&MAKE_EPHEM='YES'"                                     .
   "&EPHEM_TYPE='OBSERVER'"                                .
   "&CAL_FORMAT='BOTH'"                                    .
   "&ANG_FORMAT='DEG'"                                     .
   "&APPARENT='REFRACTED'"                                 .
   "&CENTER='COORD@399'"                                   .
   "&COORD_TYPE='GEODETIC'"                                .
   "&SITE_COORD='$LonDeg, $LatDeg, $AltKm'"                .
   "&TIME_DIGITS='SECONDS'"                                .
   "&TIME_ZONE='$TZHours'"                                 .
   "&START_TIME='$StartDate $StartTime $TimeScale'"        .
   "&STOP_TIME='$StartDate 23:59:59.999'"                  .
   "&STEP_SIZE='1 day'"                                    .
   "&EXTRA_PREC='YES'"                                     .
   "&RANGE_UNITS='AU'"                                     .
   "&SUPPRESS_RANGE_RATE='NO'"                             .
   "&QUANTITIES='2,20,13,9,4,42,7,30,23'"                  .
   "&CSV_FORMAT='YES'"                                     ;
// =========================================================

// ------------------------------------------------------------------
// Send query to Horizons API to get requested data in plain-text
// format.  All ephemerides are returned in plain-text CSV format
// to facilitate easy column parsing.

   $RawResponse = trim(File_Get_Contents($From_NASA_JPL_Horizons_API));

/*
   ###########################################################################
   At this point we now have the raw ephemeris as long as there were no erro-
   neous arguments.   However, not everything returned by the program is an
   ephemeris.

   When attempting to parse the returned data, it is first necessary to de-
   termine what kind of data was returned or it could lead to a program crash
   if not parsed correctly according to its format.

   Any returned ephemeris will be contained between 2 markers:
   $$SOE = Start Of Ephemeris
   $$EOE = End Of Ephemeris

   An ephemeris table can consist of one or multiple lines of data, but it
   will always be in between the $$SOE and $$EOE markers.

   If there is no starting marker ($$SOE), then the returned response is NOT
   an ephemeris and could possibly be an error message due to an invalid API
   calling argument.

   The raw response should be scanned for error message strings and filtered
   accordingly.

   ###########################################################################
*/

// ---------------------------------------------------------
// Strip redundant commas from end of ephemeris table lines.

   $RawResponse = Str_Replace(",\n", "\n", $RawResponse);

// -------------------------------------------
// Return the raw Horizons API response to be
// screened for errors and parsed for content.

   return $RawResponse;

} // End of  Moon_Phase (...)






// END OF PROGRAM

?>




