<?php

/*
   ###########################################################################
   Uranus, Neptune and Pluto Rise, Transit and Setting Times
   WITH 30-DAY COOKIE

   AUTHOR   : Jay Tanner - 2025
   LANGUAGE : PHP v8.2.12
   LICENSE  : Public Domain

   This program computes a table of rise, transit and setting times for the
   planets and Pluto.

   It may take several seconds to run this program, since it is computing for
   11 solar system bodies.
   ###########################################################################
*/

   
ob_start();

   
$MONTHS 'JanFebMarAprMayJunJulAugSepOctNovDec';
// ---------------------------------------------------------------
// Define the program cookie name and set it to expire in 30 days.

   
$CookieName 'Object-Rising-Transit-Setting-Times';
   
$SetToExpireIn30Days time() + 30*86400;


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



// ---------------------------------
// Define PHP program and HTML info.

   
$_AUTHOR_           'Jay Tanner of Waterloo, NY, USA';
   
$_PROGRAM_VERSION_  'v1.00 - '$at "&#97;&#116;&#32;&#76;&#111;&#99;&#97;&#108;&#32;&#84;&#105;&#109;&#101;&#32;"$LTC "&#85;&#84;&#67;";
   
$_SCRIPT_FILE_PATH_ Filter_Input(INPUT_SERVER'SCRIPT_FILENAME');
   
$_REVISION_DATE_    $_PROGRAM_VERSION_ .'Revised: 'date("Y-F-d-l $at h:i:s A   ($LTC"FileMTime($_SCRIPT_FILE_PATH_))."&minus;05:00)";
   
$_BROWSER_TAB_TEXT_ "Uranus, Neptune and Pluto Rising, Transit and Setting Times";
   
$_INTERFACE_TITLE_  "<span style='font-size:16pt; line-height:130%;'>Uranus, Neptune and Pluto<br>Rising, Transit and Setting Times</span><br><br><span style='font-size:12pt;'>Computations via The NASA/JPL Horizons API</span><br><span style='font-size:9pt;'>PHP Program by $_AUTHOR_</span>";


/* -------------------------------------
   Define main TextArea text and background
   colors and HTML table row span. If an
   error is reported, then these colors
   will change internally to red/white.
*/
   
$TxColor 'black';
   
$BgColor 'white';

/* ---------------------------------------------
   Do this only if [SUBMIT] button was bypassed
   or not clicked via direct entry from a  URL.
*/
   
$w Filter_Input(INPUT_POST'SubmitButton');

   if (!IsSet(
$w))
  {

/* ----------------------------------------------------------------------
   If this program is being called externally, rather than being executed
   by clicking the [SUBMIT] button, and an active cookie also exists,
   then restore the previously saved interface settings from it. If
   the user leaves and comes back later, all the interface settings
   will be remembered and restored if the cookie was not deleted.
*/
   
$w Filter_Input(INPUT_COOKIE$CookieName);

   if (IsSet(
$w))
      {
       
$CookieDataString Filter_Input(INPUT_COOKIE$CookieName);
       list(
$BCAD,$Year,$Month,$Day,$TimeZone,$DaySumTimeYN,$LatDeg,$LonDeg,$AltMet) = Preg_Split("[\|]"$CookieDataString);
      }

   else


/* -----------------------------------------------------------
   If there is no previous cookie with the interface settings,
   then set the  initial default interface startup values and
   store them in a new cookie. These values can be changed to
   whatever the programmer deems convenient.
*/

 
{
   
$BCAD         'AD';
   
$Year         date('Y');
   
$Month        date('M');
   
$Day          date('d');
   
$StepSize     '1 hour';
   
$TimeZone     '-05:00'// MUST be in '+-HH:MM' format.
   
$DaySumTimeYN 'No';
   
$LatDeg       '+42.904788';
   
$LonDeg       '-76.862737';
   
$AltMet       '0';

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

   
$CookieDataString "$BCAD|$Year|$Month|$Day|$TimeZone|$DaySumTimeYN|$LatDeg|$LonDeg|$AltMet";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
  } 
// End of  else {...}

  
// End of  if (!isset(_POST['SubmitButton']))


/* ------------------------------------------
   Read values of all interface arguments and
   set any empty arguments to default values.
*/
   
$w Filter_Input(INPUT_POST'SubmitButton');

   if (isset(
$w))
{
   
$BCAD StrToUpper(trim(Filter_Input(INPUT_POST'BCAD')));
           if (
substr($BCAD,0,1) == '')  {$BCAD 'AD';}
           if (
substr($BCAD,0,1) == 'B') {$BCAD 'BC';}
           if (
substr($BCAD,0,1) == 'A') {$BCAD 'AD';}

   
$Year trim(Filter_Input(INPUT_POST'Year'));
           if (
$Year == '') {$Year date('Y');}

/* -------------------------------------------
   Get month as a number (1 to 12) or as a
   3-letter abbreviation ('Jan' to 'Dec').
   NOT case-sensitive.
*/
   
$Month UCFirst(substr(StrToLower(trim(Filter_Input(INPUT_POST'Month'))),0,3));
            if (
$Month == '') {$Month date('M');}
   
$xMonth $Month;

   if (
Is_Numeric($Month) and $Month == 0) {$Month 'Jan';}

   for (
$ii=0;  $ii 1;  $ii++)
  {
   if (
Is_Numeric($Month))
      {
       
$m IntVal($Month);
            if (
$m or $m 12) {$Month 0; break;}
       
$Month substr($MONTHS3*($m-1), 3);
      }
        
$jj StrPos($MONTHS$Month);
              if (
$jj === FALSE) {$Month 0; break;}
        
$Month substr($MONTHS$jj3);
  }

// Read day of month argument.
   
$Day trim(Filter_Input(INPUT_POST'Day'));
          if (
$Day == '') {$Day date('d');}
          if (
Is_Numeric($Day)) {$Day SPrintF('%02d'$Day);}


// Read Time Zone argument.
   
$TimeZone trim(Filter_Input(INPUT_POST'TimeZone'));
               if (
$TimeZone == '') {$TimeZone '-05:00';}
   
$TimeZone HMS_to_Hours($TimeZone);
   
$TimeZone substr(Hours_to_HMS ($TimeZone0'+'':'),0,6);


// Read Daylight/Summer Time mode argument.
   
$DaySumTimeYN StrToUpper(substr(trim(Filter_Input(INPUT_POST'DaySumTimeYN')),0,1));
   
$DaySumTimeYN = ($DaySumTimeYN <> 'Y')? 'No':'Yes';


// Read geographic GPS coordinates and altitue arguments.
   
$LatDeg trim(Filter_Input(INPUT_POST'LatDeg'));
             if (
$LatDeg == '') {$LatDeg '+42.904788';}
   
$LatDeg trim(DMS_to_Degrees ($LatDeg9));
             if (
$LatDeg >= 0) {$LatDeg "+$LatDeg";}
   
$LatDeg Str_Replace('++''+'$LatDeg);
   
$LonDeg trim(Filter_Input(INPUT_POST'LonDeg'));
             if (
$LonDeg == '') {$LonDeg '-76.862737';}
   
$LonDeg trim(DMS_to_Degrees ($LonDeg9));
   
$AltMet trim(Filter_Input(INPUT_POST'AltMet'));
             if (
$AltMet == '') {$AltMet '0.0';}
   
$AltMet SPrintF("%+1.1f"$AltMet);

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

   
$CookieDataString "$BCAD|$Year|$Month|$Day|$TimeZone|$DaySumTimeYN|$LatDeg|$LonDeg|$AltMet";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
}



/* -------------------------------------------------------------------
   These are error filters to check some input arguments for validity.
   If errors are detected, then error flags and messages are set.
*/
   
$ErrFlag FALSE;
   
$ErrMssg '';


// Check Month value.
   
if ($Month == '0')
      {
       
$ErrFlag TRUE;
       
$ErrMssg "Bad month number or abbreviation.\n'$xMonth'\n\nThe month must be an integer from 1 to 12 = (Jan to Dec)\nor a 3-letter abbreviation from 'Jan' to 'Dec'.";
       
$Month $xMonth;
      }

// Check Year value.
   
if (!Is_Numeric($Year) or $Year == or $Year 9999)
      {
       
$ErrFlag TRUE;
       
$ErrMssg "Bad year number.\n'$Year'\n\nThe year must be a non-zero integer &le; 9999.";
      }

// Check BC/AD era value.
   
if ($BCAD <> 'BC' and $BCAD <> 'AD')
      {
       
$ErrFlag TRUE;
       
$ErrMssg "'$BCAD'\nBad BC/AD era symbol.\n\nThe era symbol must be BC or AD only.\nAn empty input defaults to AD era.";
      }

// Check Day value.
   
if (!Is_Numeric($Day) or (Is_Numeric($Day) and $Day or $Day 31))
      {
       
$ErrFlag TRUE;
       
$ErrMssg "Bad day number.\n'$Day'\n\nThe day must be a positive integer &le; 31.";
      }

// --------------------------------
// Define E/W/N/S direction Labels.

   
$EWLabel = ($LonDeg 0)? "&plus;East":"&minus;West";
   
$NSLabel = ($LatDeg 0)? "&minus;South":"&plus;North";
   
$TZLabel = (substr($TimeZone,0,1) == '-')? "&plus;East":"&minus;West";

// -----------------------------
// Set initial uniform width for
// tables alignment in pixels.

   
$TableWidth '780';


// ******************************************
// ******************************************
// If error was reported (TRUE), then display
// the error message on a red background.

   
if ($ErrFlag === TRUE)
  {
   
$TxColor 'white';
   
$BgColor '#CC0000';
   
$TextArea2Text '';

   
$TextArea1Text =
"=== ERROR ===

$ErrMssg";
  }

else


  {
// *********************************************************
// BEGIN MAIN COMPUTATIONS HERE IF NO ERRORS DETECTED ABOVE.
// *********************************************************



// ----------------------------------------------
// Force year value into positive 4-digit format.

   
$Year SPrintF("%04d"abs($Year));

/* ---------------------------------------------------------------
   Define list of Body IDs to process. Processing can take several
   seconds, depending on the number of bodies in the listing.
*/

   
$LocLabel 'Waterloo, NY, USA';

   
$BodyIDList '799  Uranus, 899  Neptune, 999  Pluto';


// -------------------------------------------------
// Construct calling date argument for the function.

   
$DateStr "$BCAD $Year-$Month-$Day";

// --------------------------------------------------
// Generate the RTS table for the bodies in the list.

   
$RTSTimesTable Bodies_RTS ($BodyIDList,$DateStr,$TimeZone,$DaySumTimeYN,
                                
$LocLabel,$LatDeg,$LonDeg,$AltMet);

// ---------------------------------------------
// Account for certain ephemeris error messages.

   
$RTSTimesTable Str_Replace('prior to '"prior to\n"$RTSTimesTable);

   
$RTSTimesTable Str_Replace('Cannot interpret date. Type "?!" or try YYYY-MMM-DD {HH:MN} format.'"ERROR:\nAt least one of the given date elements is invalid.\n\n'$BCAD $Year-$Month-$Day'"$RTSTimesTable);

   
$RTSTimesTable Str_Replace(' after A.D. '" after\nA.D. "$RTSTimesTable);

/*
   *******************************************
   DROP THROUGH HERE AFTER COMPUTATIONS ABOVE
   TO PRINT OUT THE RESULTS OF THE OPERATIONS.
   *******************************************
*/

// Construct main output TextArea1 text block.

   
$TextArea1Text =
"                     URANUS, NEPTUNE AND PLUTO
                  RISING, TRANSIT AND SETTING TIMES

ERA Year-Month-Day   =  
$BCAD $Year-$Month-$Day
Time Zone            =  UT
$TimeZone  ($TZLabel)
Daylight/Summer Time =  
$DaySumTimeYN

Latitude  GPS        = 
$LatDeg &deg;  ($NSLabel)
Longitude GPS        = 
$LonDeg &deg;  ($EWLabel)
Altitude in meters   = 
$AltMet m

$RTSTimesTable
"
;
  }




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

   
$TextArea2Text =
"                            GENERAL INFORMATION

               Ephemeris span for Uranus  : AD 1600 to AD 2399
               Ephemeris span for Neptune : AD 1600 to AD 2400
               Ephemeris span for Pluto   : AD 1800 to AD 2199

               See:
               https://ssd.jpl.nasa.gov/horizons/time_spans.html

IMPORTANT NOTE:
Make sure that the given  geographic GPS coordinates  are within the given
Time Zone.  The time zone and local coordinates must match or the computed
times will NOT be correct.
"
;



/* **************************************************************************
   Determine number of text columns and rows to use in the output text areas.
   These values vary randomly according to the text block width and length.
   The idea is to eliminate the need for scroll-bars within the text areas
   or worry as much about the variable dimensions of a text display area.
*/

// ---------------------------
// TextArea1 columns and rows.

   
$Text1Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea1Text))))-9;
//   if ($Text1Cols < 80) {$Text1Cols = 60;}

   
$Text1Rows Substr_Count($TextArea1Text"\n");

// ---------------------------
// TextArea2 columns and rows.

   
$Text2Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea2Text))));
   if (
$Text2Cols 80) {$Text2Cols 75;}
   
$Text2Rows Substr_Count($TextArea2Text"\n");



// ******************************************
// ******************************************
// GENERATE CLIENT WEB PAGE TO DISPLAY OUTPUT

   
print <<< _HTML

<!DOCTYPE HTML>
<HTML>

<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='Solar System Rise, Transit and Setting Times'>
<meta name='keywords' content='PHPScienceLabs.com'>
<meta name='author' content='Jay Tanner - https://www.PHPScienceLabs.com'>
<meta name='robots' content='index,follow'>
<meta name='googlebot' content='index,follow'>

<style>

 BODY {color:white; background:black; font-family:Verdana; font-size:12pt; line-height:125%;}

 TABLE
{font-size:13pt; 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
{
 background:white; color:black; font-family:Verdana; font-size:11pt;
 font-weight:normal; line-height:125%; padding:6px;
}


 TEXTAREA
{
 background:white; color:black; font-family:monospace; font-size:13pt;
 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:13pt;
 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:13pt; border:2px solid blue; text-align:center; font-weight:bold;
 border-radius:4px;
}



 INPUT[type='submit']
{
 background:black; color:cyan; font-family:Verdana; font-size:10pt;
 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:10pt;
 font-weight:bold; border-radius:4px; border:4px solid red;
 padding:3pt;
}





// Link states MUST be set in the following order:
// :link, :visited, :hover, :active

 A:link
{
 font-size:10pt; background:transparent; color:#8080FF; 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:DarkCyan; 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>

<br>

<!-- Define container form --->
<form name="form1" method="post" action="">

<!-- Define main page title/header. --->
<table width="
$TableWidth" align="center" 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>


<!-- Define input  text boxes  --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr><td style="background:LightYellow; line-height:175%;">
Date <input name="BCAD"          type="text" value="
$BCAD"      size="3" maxlength="2">
<input name="Year"               type="text" value="
$Year"      size="5" maxlength="4">
<input name="Month"              type="text" value="
$Month"     size="4" maxlength="3">
<input name="Day"                type="text" value="
$Day"       size="3" maxlength="2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Time Zone <input name="TimeZone" type="text" value="
$TimeZone"  size="7" maxlength="6">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Daylight/Summer Time <input name="DaySumTimeYN" type="text" value="
$DaySumTimeYN" size="4" maxlength="3">

</td></tr>
</table>



<!-- LONGITUDE/LATITUDE/ALTITUDE SETTINGS TABLE --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr title=" IMPORTANT:\n Make sure that the given Geographic Coordinates are&nbsp; within the \n given Time Zone or the computed times will not be accurate. \n ">

<!-- GPS Latitude in Degrees or DMS --->
<td width='33%' style='background:#E0FFE0; line-height:180%; border-radius:0px 0px 0px 8px;'>&plusmn; GPS Latitude &nbsp;&nbsp;&nbsp;&plus;Pos = N
<input name="LatDeg"   type="text" value="
$LatDeg"  size="17" maxlength="16" title=' GPS Latitude in Decimal Degrees\nor as\n Deg Min Sec   Separated by Spaces. \n'></td>


<!-- GPS Longitude in Degrees or DMS --->
<td width='33%' style='background:#E0FFE0; line-height:180%;'>&plusmn; GPS Longitude &nbsp;&nbsp;&nbsp;&plus;Pos = E
<input name="LonDeg"  type="text" value="
$LonDeg" size="17" maxlength="16" title=' GPS Longitude in Decimal Degrees\nor as\n Deg Min Sec   Separated by Spaces \n'></td>


<!-- Sea level altitude --->
<td style='background:#E0FFE0; line-height:180%; border-radius:0px 0px 8px 0px;'>&plusmn; Sea Level Altitude in Meters
<input  name="AltMet"  type="text" value="
$AltMet"  size="9" maxlength="8" title=' Altitude in meters relative to sea level. '>
</td>

</tr>
</table>




<!-- Define [SUBMIT] button --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr><td colspan="99" style="background-color:black;"><input type="submit" name="SubmitButton" value=" S U B M I T " OnClick="
$_COMPUTING_"></td></tr>
</table>


<!-- Yellow source code view link. --->
<table width='550' align='center' cellspacing='1' cellpadding='3'>
<!-- Yellow source code view link. --->
<tr>
<td colspan='1' style='background:transparent; color:black; font-size:10pt;
                       text-align:center;'>
<a href='View-Source-Code.php' target='_blank'
   style='font-family:Verdana; color:black; background:yellow;
         text-decoration:none; border:1px solid black; padding:4px;
         border-radius:4px;'>
         &nbsp;<span style='font-weight:normal;'>View/Copy PHP Source Code</span>&nbsp;</a>
</td>
</tr>
</table>




<!-- Define TextArea1 --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:center; color:GreenYellow; background-color:black;">Double-Click Within Text Area to Select ALL Text<br>
<textarea ID="TextArea1" name="TextArea1" style="color:
$TxColor; background:$BgColor; padding:6px; border:2px solid white;" cols="$Text1Cols" rows="$Text1Rows" ReadOnly OnDblClick="this.select();" OnMouseUp="return true;">
$TextArea1Text
</textarea>
</td>
</tr>
</table>


<!-- Define TextArea2 --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:center; color:GreenYellow; background:black;">Double-Click Within Text Area to Select ALL Text<br>
<textarea ID="TextArea2" name="TextArea2" style="color:black; background:white; padding:6px;" cols="
$Text2Cols" rows="$Text2Rows" ReadOnly OnDblClick="this.select();" OnMouseUp="return true;">
$TextArea2Text
</textarea>
</tr>
</table>


<!-- Define page footer --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="color:GreenYellow; background:black;">PHP Program by 
$_AUTHOR_<br><span style="color:silver; background:black;">$_REVISION_DATE_</span></td>
</tr>
</table>

</form>
<!-- End of container form --->


<!-- Extra bottom scroll space --->
<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>



_HTML;









/*
   This function constructs a table of rise, transit and setting times
   for a list of any given objects for which an apparent topocentric
   ephemeris can be computed.
*/

  
function Bodies_RTS ($BodyIDList,$DateStr,$TimeZone,$DaySumYN,
                       
$LocLabel,$LatDeg,$LonDeg,$AltMet)
{
   GLOBAL 
$OBJ;

   
$LocLabel trim($LocLabel);  if ($LocLabel == '') {$LocLabel '---';}

   
$wArray PReg_Split("[,]"trim($BodyIDList));
   
$wCount count($wArray);

   
$out '';

   for (
$i=0;   $i $wCount;   $i++)
  {
   
$CurrBodyID trim($wArray[$i]);
   
$w R_T_S_Times ($CurrBodyID,$DateStr,$TimeZone,$DaySumYN,
                     
$LocLabel,$LatDeg,$LonDeg,$AltMet);

   
$out .= "$w\n\n";
  }

  return 
trim($out);
}





/*
   This function returns a compass direction string
   for any given azimuth (direction) angle.
*/

   
function Compass_Symbol ($AzimDeg)
{
   
$ZModeN0 'N  NNENE ENEE  ESESE SSES  SSWSW WSWW  WNWNW NNWN';

   
$a trim($AzimDeg);  if (!Is_Numeric($a)) {return FALSE;}

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

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

   
$w trim(substr($ZModeN03*$i3));
//   if (StrLen($w) == 1) {$w = " $w ";} // ' N '
//   if (StrLen($w) == 2) {$w = " $w ";} // 'NW '

   
return $w;

// End of  Compass_Symbol (...)






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

   INPUT: 1, 2 or 3-element DMS (Degrees Minutes Seconds) separated by spaces
          as angular string as 'deg min sec' values.

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

   NO DEPENDENCIES

   ERRORS:
   No error checking is done.
   ###########################################################################
*/

   
function DMS_to_Degrees ($DMSstr$Decimals=16)
{
   
$dms StrToLower(trim($DMSstr));

   
$Q 40;
   
$q trim($Decimals);

/* ----------------------------------------------------
   Normalize the spacing and then split and extract the
   individual angular string elements (dh or hh, mm,ss).
*/
   
$dms PReg_Replace("/\s+/"" "trim($dms));
   
$wdms PReg_Split("[ ]"$dms);
   
$wdmscount count($wdms);
   
$dd = ($wdmscount >= 1)? bcAdd($wdms[0],"0"$Q) : "0";
   
$mm = ($wdmscount >= 2)? bcAdd($wdms[1],"0"$Q) : "0";
   
$ss = ($wdmscount >= 3)? bcAdd($wdms[2],"0"$Q) : "0";

// ------------------------------------------------
// 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 '';}

// -------------------------------------------------------
// Return result to (q) decimals, recalling original sign.

   
$z Str_Repeat('0'$q);
   
$w $NumSign bcAdd(bcDiv($w2,"3600",$Q), "0.".$z."5"$q);
// $w = RTrim(RTrim($w, '0'), '.');

   
return $w;

// End of  DMS_to_Degrees (...)





/*
   ============================================================================
   This function converts a time elements string ('h m s') to decimal hours.

   NO DEPENDENCIES
   ============================================================================
*/

   
function HMS_to_Hours ($hhmmss)
{
// Read hour angle string argument and force to lower case in
// the event any (h m s :) symbols are used.
   
$hms StrToLower(trim($hhmmss));

// Convert any (h m s :) symbols into spaces.
   
$hms Str_Replace('h'' '$hms);
   
$hms Str_Replace('m'' '$hms);
   
$hms Str_Replace('s'' '$hms);
   
$hms Str_Replace(':'' '$hms);

// Normalize the spacing and then split and extract the
// individual time string elements (hh, mm,ss).
   
$hms PReg_Replace("/\s+/"" "trim($hms));
   
$whms PReg_Split("[ ]"$hms);
   
$whmscount count($whms);
   
$hh = ($whmscount >= 1)? bcAdd($whms[0],"0"20) : "0";
   
$mm = ($whmscount >= 2)? bcAdd($whms[1],"0"20) : "0";
   
$ss = ($whmscount >= 3)? bcAdd($whms[2],"0"20) : "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.
   
if (substr($hhmmss,0,1) == '+') {$NumSign '+';}

// Compute decimal hours value equivalent
// to the combined hh,mm,ss values.
   
$w2 bcAdd(bcAdd(bcMul($hh,'3600',20),bcMul($mm,'60',20),20),$ss,20);

// If result equates to zero, then suppress any numerical sign.
   
if (bcComp($w2'0'20) == 0) {$NumSign '';}

// round off result to 16 decimals.
   
$w =  bcAdd(bcDiv($w2,360020), '0.00000000000000005'16);

// Reattach numerical sign and trim off
// any redundant zeros/decimal point.
   
return $NumSign.Rtrim(Rtrim($w'0'), '.');

// End of  HMS_to_Hours (hhmmss)




/*
   ===========================================================================
   This function returns the equivalent H:M:S time string with
   several formatting 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 == or $SymbMode == '')
      {
$_h_  =  $_m_  =  $_s_  ' ';}

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

   if (
$SymbMode == 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 returns a topocentric ephemeris of rise/transit/set times
   from the NASA/JPL Horizons API. A topocentric ephemeris takes the location
   and altitude of the observer into account. Standard Earth refraction model
   is applied.

   NOTE: The BodyID MUST be an integer record # (301)
         For asteroids, add a semicolon (;) to the record #. (301;)

   DaySumYN = Means Daylght Saving/Summer Time 'Yes|No'
             'No'  = Use Standard Clock Time (Default)
             'Yes' = Use Daylight Saving/Summer Clock Time

   Angles are expressed in degrees.


COLUMN  CONTENT
======  =======================================================================
  1     Date and Time String (UT or ZONE)
  2     Sp (Solar Presence Symbol)
  3     Lp (Lunar Presence Symbol)
  4     Azimuth Angle (Compass Direction Reckoned Clockwise From North = 0)
  5     Elevation Angle (Relative to Horizon = 0 degrees)
  6     Apparent Visual Magnitude or Brightness Level
  7     S-brt (Brightness of 1 Square Arcsecond of Visible Disc)

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

   
function R_T_S_Times ($BodyID,$DateStr,$TimeZone,$DaySumYN,
                         
$LocLabel,$LatDeg,$LonDeg,$AltMet)
{
// ===========================================================================
// Construct query URL for the NASA/JPL Horizons API.

   
GLOBAL $OBJ;

   
$OBJ $BodyID trim($BodyID);

   if (
strPos($BodyID';') === FALSE)
      {
$BodyID IntVal(trim($BodyID));}
   else
      {
$BodyID IntVal(trim($BodyID)) . ';';}


   
$Command  URLEncode($BodyID);
   
$AltKm    trim($AltMet) / 1000;
   
$LocLabel trim($LocLabel);  if ($LocLabel == '') {$LocLabel '---';}

/* -----------------------------------------------------------
   Adjust for Daylight/Summer Time, if indicated. This assumes
   that the Time Zone string is given in the standard +-HH:mm
   format or an error may occur.
*/
   
$DaySumYN  substr(StrToUpper(trim($DaySumYN)),0,1);
   
$DaySumAdj = ($DaySumYN == 'N')? 0:1;
   list(
$TZHH$TZmm) = PReg_Split("[\:]"$TimeZone);
   
$TZSign substr($TZHH,0,1);
   
$TZHours = (($TZSign == '-')? -1:1)*(abs($TZHH) + $TZmm/60) + $DaySumAdj;
   
$i StrPos($TZHours'.');  if ($i == FALSE) {$TZHours .= '.00';}
   
$i StrPos($TZHours'.');
   
$TZHH $TZSign.SPrintF("%02d"abs(substr($TZHours,0,$i)));
   
$TimeZone "$TZHH:$TZmm";

// -------------------------------------
// Construct URL for Horizons API query.

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='$Command'"                      .
   
"&OBJ_DATA='NO'"                           .
   
"&MAKE_EPHEM='YES'"                        .
   
"&EPHEM_TYPE='OBSERVER'"                   .
   
"&CAL_FORMAT='CAL'"                        .
   
"&REF_SYSTEM='ICRF'"                       .
   
"&APPARENT='REFRACTED'"                    .
   
"&CENTER='COORD@399'"                      .
   
"&COORD_TYPE='GEODETIC'"                   .
   
"&SITE_COORD='$LonDeg,$LatDeg,$AltKm'"     .
   
"&TIME_DIGITS='MINUTES'"                   .
   
"&TIME_ZONE='$TimeZone'"                   .
   
"&START_TIME='$DateStr%2000:00:00%20UT'"   .
   
"&STOP_TIME='$DateStr%2023:59:59'"         .
   
"&STEP_SIZE='1 MINUTE'"                    .
   
"&EXTRA_PREC='YES'"                        .
   
"&R_T_S_ONLY='TVH'"                        .
   
"&QUANTITIES='4,9'"                        .
   
"&CSV_FORMAT='YES'"                        ;
// ============================================

/* -----------------------------------------------------------------------
   Send query to Horizons API to obtain the apparent topocentric ephemeris
   data we need for the R-T-S times of the given body ID.
*/
   
$RTS Str_Replace(",\n"" \n"File_Get_Contents($From_Horizons_API));

   
$RTS trim($RTS);

/* ----------------------------------------------------------------------
   If no ephemeris data is found, then return an empty string as an error
   state indicator.
*/
   
if (StrPos($RTS'$$SOE') === FALSE)
      {
       return
"=======================================================
$OBJ\n\n$RTS";
      }

/* -------------------------------------------------------------------------
   DO NOT TRIM HERE BECAUSE INITIAL CHARACTER MAY BE A SPACE INDICATING AN
   'AD' YEAR.  'BC' YEARS BEGIN WITH A SINGLE 'b' AS THE INITIAL CHARACTER.

   BAD PRACTICE. LEADING/TRAILING SPACES SHOULD NOT BE USED AS TABLE DATA.
   IT CAN SOMETIMES LEAD TO CONFUSION WHEN READING A LINE OF TABULAR TEXT
   BY AN AUTOMATED PROCESS.

   IF A DATA COLUMN IS EMPTY, A SPECIAL CHARACTER SHOULD BE ASSIGNED AS AN
   INDICATOR OF AN EMPTY COLUMN.  EXAMPLE (~), SIMILAR TO USING (n/a).
   SOMETHING OTHER THAN SIMPLY LEAVING IT EMPTY OR USING A SPACE CHARACTER.

   DIFFERENT PROGRAMMING LANGUAGES, WHEN READING THE TABLE, MAY NOT TREAT
   LEADING OR TRAILING SPACES THE SAME WAY AND THEY MAY POSSIBLY BE LOST.

   THIS HAS CAUSED PROBLEMS IN THE PAST WHEN USING THE TABLES WITH OTHER
   PROGRAMMING LANGUAGES CAUSING DATA TO BE SKIPPED OR MIS-READ MAKING IT
   NECESSARY TO PROGRAMMATICALLY ACCOUNT FOR IT.
*/

/* -----------------------------------------------------
   Set pointers to start and end of CSV ephemeris table.
   A value of FALSE means no ephemeris was found within
   the returned text.  Instead, it could be an error or
   other message.
*/
   
$j StrPos($RTS'$$SOE');
   
$k StrPos($RTS'$$EOE');

   
$RawRTSTable RTrim(substr($RTS$j+5$k-$j-5));

// Construct custom formatted RTS table.

   
$wArray PReg_Split("[\n]"trim($RawRTSTable));
   
$wCount count($wArray);

   
$out '';

   for (
$i=0;   $i $wCount;   $i++)
  {
   
$CurrLine trim($wArray[$i]);
   
$out .= "$CurrLine\n";
  }

   
$out2   '';
   
$uArray PReg_Split("[\n]"trim($out));
   
$uCount count($uArray);

   for(
$j=0;   $j $uCount;   $j++)
   {
    
$X trim($uArray[$j]);
         list
        (
         
$DateTimeStr,
         
$w,
         
$RorTorS,
         
$azim,
         
$elev,
         
$VMag
        
) = PReg_Split("[,]"$X);

// -------------------
// Get RTS indicators.
   
$DateTimeStr Str_Replace(' ''  '$DateTimeStr);

   if (
$RorTorS == 'r') {$RorTorS 'Rises   ';}
   if (
$RorTorS == 't') {$RorTorS 'Transits';}
   if (
$RorTorS == 's') {$RorTorS 'Sets    ';}

// Handle BC/AD era symbol.
   
if (substr($DateTimeStr,0,1) == 'b')
      {
$DateTimeStr 'BC ' substr($DateTimeStr1StrLen($DateTimeStr));}
   if (
Is_Numeric(substr($DateTimeStr,0,1)))
      {
$DateTimeStr "AD $DateTimeStr";}


// ---------------------------------------------
// Handle azimuth and compass direction symbols
    
$azim SPrintF("% 7.2f"$azim);
    
$AzimStr trim(Compass_Symbol($azim));
    if (
StrLen($AzimStr) == 1) {$AzimStr $AzimStr ";}
    if (
StrLen($AzimStr) == 2) {$AzimStr "$AzimStr ";}


// ---------------------------------------------------
// Handle 'Transit' azimuth marker and azimuth string.
   
if ($RorTorS == 'Transits') {$azim '    |  ';  $AzimStr "$AzimStr ";}


/* -------------------------------
   Handle horizon elevation angle.
   < 0 = Below Horizon
     0 = Horizon
   > 0 = Above Horizon

*/

   
if ($RorTorS <> 'Transits')
      {
$elev '   -  ';}
 else
      {
$elev SPrintF("% 5.2f"$elev);}


/* ------------------------------------------------------
   Attach numerical + sign to positive visual magnitudes
   unless the value is non-numeric
*/
   
$VMag trim($VMag);
   if (
Is_Numeric($VMag))
      {
$VMag SPrintF("%+7.3f"$VMag);}
   else
      {
$VMag "&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;";}

// --------------------------------
// Append line to constructd table.

   
$out2 .= "$DateTimeStr  $RorTorS  $azim  $AzimStr $elev    $VMag\n";
   }

// ----------------------------------------
// Return customized RTS table with header.

   
$w =
"===============================================================
$OBJ

Calendar_Date   Time    Event     Azim&deg;   Dir  Elev&deg;    Vis_Mag
--------------  -----  --------   ------  ---  -----    -------
$out2
"
;

   return 
trim($w);

// End of  R_T_S_Times(...)








// END OF PROGRAM


?>