<?php

/*
   ###########################################################################
   Voyagers 1 and 2 Hourly Ephemerides

   This program computes the hour-by-hour geocentric and heliocentric
   statistics of Voyagers 1 and 2 for any given hour since their
   launch dates.

   This way, you can study the historical progress of both Voyagers
   over the years and even make projections into the future.

   ########################################################
   ONE LIGHT DAY PROJECTIONS UTC VIA NASA/JPL HORIZONS API:

   Voyager 1 = 1 Light Day From Earth     Date: 2026-Nov-19-Thu
   Voyager 2 = 1 Light Day From Earth     Date: 2035-Nov-01-Thu
   ------------------------------------------------------------
   Voyager 1 = 1 Light Day From Sun       Date: 2027-Feb-04-Thu
   Voyager 2 = 1 Light Day From Sun       Date: 2035-Nov-28-Wed

    ------------------------
    SOME BASIC APPLIED DATA:

  * 1 AU            = 149,597,870.7 km     =  92,955,807.3 mi
  * Speed of Light  = 299,792.458 km/s     =  186,282.397 mi/s
    1 Light Day     = 25,902,068,371.2 km  =  16,094,799,105.2 mi
  * 1 statute mile  = 1.609344 km

  * (asterisk) Means an EXACT DEFINITION

   AUTHOR   : Jay Tanner - 2026
   LANGUAGE : PHP v8.2.12
   LICENSE  : Public Domain
   ###########################################################################
*/

   
ob_start(); // Oy!

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

   
$CookieName 'Voyagers-1-and-2-Hourly-Ephemerides';
   
$SetToExpireIn30Days time() + 30*86400;

// ------------------------------------------------------------------
// Define JavaScript message to display in (TextArea1) while working.

   
$_COMPUTING_ "TextArea1.innerHTML='          W O R K I N G  ---  This may take a few moments.';";

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

   
$_AUTHOR_           'Jay Tanner of Geneva, 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_ "Voyagers 1 and 2 Hourly Ephemerides";
   
$_INTERFACE_TITLE_  "<span style='font-size:15pt;'>Voyagers 1 and 2 Hourly Ephemerides</span><br><span style='font-size:12pt;'>Built Around The NASA/JPL Horizons API</span><br><br><span style='font-size:10pt;'>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';

/* ----------------------------------------
   Define 3-letter month name abbreviations
   for use with calendar computations.
*/
   
define('MONTHS''JanFebMarAprMayJunJulAugSepOctNovDec');

/* ------------------------------------------
   Define 3-letter weekday name abbreviations
   for use with calendar computations.
*/
   
define('DOWs''SunMonTueWedThuFriSat');

// ------------------------------------------------
// Do this only if [SUBMIT] button was NOT clicked.

   
$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.
   Otherwise, the user would have to re-enter every parameter from
   scratch for each and every computation because they would all be
   forgotten after each usage.
*/
   
$w Filter_Input(INPUT_COOKIE$CookieName);

   if (IsSet(
$w))
      {
       
$CookieDataString Filter_Input(INPUT_COOKIE$CookieName);
       list (
$Year$Month$Day)
       = 
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.
*/

 
{
   
$Year  GMDate('Y');
   
$Month GMDate('M');
   
$Day   GMDate('d');

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

   
$CookieDataString "$Year|$Month|$Day";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
  } 
// End of  else {...}

  
// End of  if (!isset(...))


// ------------------------------------------
// Read values of all interface arguments and
// set any empty arguments to default values.

   
$w Filter_Input(INPUT_POST'SubmitButton');

   if (isset(
$w))
{
   
$Year  trim(Filter_Input(INPUT_POST'Year'));
                 if (!
Is_Numeric($Year)) {$Year GMDate('Y');}
   
$Year  SPrintF("%4d"$Year);

/* -------------------------------------------
   Get START 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 GMDate('M');}

   if (
Is_Numeric($Month) and $Month == 0) {$Month GMDate("M");}

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

   
$Day  trim(Filter_Input(INPUT_POST'Day'));
                if (
$Day == '') {$Day IntVal(GMDate('d'));}
   
$Day  SPrintF("%02d"$Day);


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

   
$CookieDataString "$Year|$Month|$Day";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
}

/* -----------------------------------------------------------
   Put code here to optionally  check individual arguments for
   validity in reverse input order.  If errors found, then set
   error flag and message values accordingly.

   NOTE: Horizons will catch some errors, such as invalid date
   errors.
*/
   
$ErrorReported FALSE;
   
$ErrMssg '';


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

   
$TableWidth '680';


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

   
if ($ErrorReported)
  {
   
$TxColor 'white';
   
$BgColor '#CC0000';
   
$TextArea2Text '';

   
$TextArea1Text =
"=== ERROR ===

$ErrMssg";
  }

else

  {

// ------------------------------------------------------
// Construct full date and time strings for Horizons API.

   
$StartDateTime =  "$Year-$Month-$Day  00:00:00 UT";
   
$StopDateTime  =  "$Year-$Month-$Day  23:59:59";

// ---------------------
// Define table headers.

   
$OutputTextHeaderG "\nCALENDAR_DATE_UTC | DISTANCE_AU | RAD_VEL_mi/h |   LIGHT_TIME\n------------------|-------------|--------------|----------------";
   
$OutputTextHeaderH "\nCALENDAR_DATE_UTC | DISTANCE_AU | RAD_VEL_mi/h |   LIGHT_TIME\n------------------|-------------|--------------|----------------";

// ------------------------------------------------------------------------------
// Compute the geocentric and heliocentric ephemeris statistics for the Voyagers.

   
$V1G Voyager_G ('Voyager 1'$StartDateTime$StopDateTime$OutputTextHeaderG);
   
$V2G Voyager_G ('Voyager 2'$StartDateTime$StopDateTime$OutputTextHeaderG);
   
$V1H Voyager_H ('Voyager 1'$StartDateTime$StopDateTime$OutputTextHeaderH);
   
$V2H Voyager_H ('Voyager 2'$StartDateTime$StopDateTime$OutputTextHeaderH);

// -------------------------------------------
// Modify certain error messages, if detected.

   
$BadDateErr  "Cannot interpret date.";
   
$BadDateStr  "ERROR: Check the given calendar date.\nThe date  $Year-$Month-$Day  is not a valid date on the real calendar.\n";
   
$SpanErr "No ephemeris for target";
   
$SpanStr "No ephemeris is available for the given date.";

   if (
StrPos($V1G$BadDateErr) !== FALSE)
      {
$OutputTextHeaderG '';  $V1G $BadDateStr;}

   if (
StrPos($V2G$BadDateErr) !== FALSE)
      {
$OutputTextHeaderG '';  $V2G $BadDateStr;}

   if (
StrPos($V1H$BadDateErr) !== FALSE)
      {
$OutputTextHeaderH '';  $V1H $BadDateStr;}

   if (
StrPos($V2H$BadDateErr) !== FALSE)
      {
$OutputTextHeaderH '';  $V2H $BadDateStr;}

   if (
StrPos($V1G$SpanErr) !== FALSE)  {$OutputTextHeaderG '';}
   if (
StrPos($V2G$SpanErr) !== FALSE)  {$OutputTextHeaderG '';}
   if (
StrPos($V1H$SpanErr) !== FALSE)  {$OutputTextHeaderH '';}
   if (
StrPos($V2H$SpanErr) !== FALSE)  {$OutputTextHeaderH '';}

   
$V1G trim(Str_Replace('prior to '"prior to\n"$V1G));
   
$V2G Str_Replace('prior to '"prior to\n"$V2G);
   
$V1H Str_Replace('prior to '"prior to\n"$V1H);
   
$V2H Str_Replace('prior to '"prior to\n"$V2H);

   
$V1G trim(Str_Replace('after '"after\n"$V1G));
   
$V2G Str_Replace('after '"after\n"$V2G);
   
$V1H Str_Replace('after '"after\n"$V1H);
   
$V2H Str_Replace('after '"after\n"$V2H);

// *********************************************************
// CONSTRUCT OUTPUT TEXT BLOCKS BASED ON COMPUTATIONS ABOVE.
// *********************************************************

   
$TextArea1Text =
"                  VOYAGERS 1 and 2 HOURLY EPHEMERIS
          FROM BOTH GEOCENTRIC and HELIOCENTRIC PERSPECTIVES

################################################################
VOYAGER 1:    GEOCENTRIC statistics for each hour of 
$Year-$Month-$Day
$OutputTextHeaderG
$V1G

----------------------------------------------------------------
VOYAGER 2:    GEOCENTRIC statistics for each hour of 
$Year-$Month-$Day
$OutputTextHeaderG
$V2G

################################################################
################################################################
HELIOCENTRIC

VOYAGER 1:  HELIOCENTRIC statistics for each hour of 
$Year-$Month-$Day
$OutputTextHeaderH
$V1H
----------------------------------------------------------------
VOYAGER 2:  HELIOCENTRIC statistics for each hour of 
$Year-$Month-$Day
$OutputTextHeaderH
$V2H
"
;
  }


// ****************************
// Define TextArea2 text block.

   
$TextArea2Text =
"                               GENERAL PROGRAM INFO
                    Computations via the NASA/JPL Horizons API

This program computes both the geocentric and heliocentric statistics for
Voyager 1 and Voyager 2 for any date since the date of their launches.

It can  be used to  compute their  historical positional  statistics since the
date of launch to the current date and  can also be used to find the projected
dates when the two Voyagers reach a given distance from both the Earth and the
Sun, such as when they will be one light day distant from them.

##############################################################################
SOME BASIC APPLIED DATA:

* 1 AU            = 149,597,870.7 km     =  92,955,807.3 mi
* Speed of Light  = 299,792.458 km/s     =  186,282.397 mi/s
  1 Light Day     = 25,902,068,371.2 km  =  16,094,799,105.2 mi
* 1 statute mile  = 1.609344 km

* (asterisk) Means an EXACT DEFINITION

##############################################################################
COLUMNS DATA
------------
DISTANCE_AU  : Distance in Astronomical Units from Earth/Sun

RAD_VEL_mi/h : Radial velocity in mi/h.  -Neg = Approaching;   +Pos = Receding
               This is the speed at which the spacecraft is approaching or re-
               ceding from the Earth or Sun.

SPECIAL NOTE : When the Earth in its orbit is traveling in the same direction
as an outgoing Voyager,  the radial velocity is a negative value  which means
it is approaching the Earth, rather than receding. This is an illusion caused
by the fact that the Earth, at a certain point in its orbit, is moving faster
than the Voyager is receding away from us in the same general direction.

LIGHT_TIME   : Light or signal time from Spacecraft to observation point.
"
;



/* --------------------------------------------------------------------------
   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.
*/

// --------------------------------------------
// Text Area 1 - Default = At least 80 columns.

   
$Text1Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea1Text))));
   if (
$Text1Cols 180) {$Text1Cols 67;}  // Default
   
$Text1Rows Substr_Count($TextArea1Text"\n");

// --------------------------------------------
// Text Area 2 - Default = At least 80 columns.

   
$Text2Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea2Text))));
   if (
$Text2Cols 80) {$Text2Cols 80;} // Default
   
$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; chrset=UTF-8'>
<meta http-equiv='pragma'       content='no-cache'>
<meta http-equiv='expires'      content='-1'>
<meta name='description'        content='Voyagers 1 and 2 Statistics For Any Date'>
<meta name='keywords'           content='NeoProgrammics  / PHP Science Labs'>
<meta name='author'             content='Jay Tanner - https://www.NeoProgrammics.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:12pt;
 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: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>

<!-- 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 calendar date text box  --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td style='background:LightYellow; line-height:150%;'><b>Calendar Date</b><br>yyyy &nbsp;Mmm&nbsp; dd<br><input name="Year"  type="text" value="
$Year" size="6" maxlength="5"><input name="Month"  type="text" value="$Month" size="4" maxlength="3"><input name="Day"  type="text" value="$Day" size="3" maxlength="2"></td>
</tr>


</table>



<!-- Top yellow source code view link. --->
<br>
<table width="
$TableWidth" align="center" cellspacing="1" cellpadding="3">
<tr>
<td colspan="1" style='font-size:10pt; color:black; background:black;
                       text-align:center;' title=' Tries to Open in a New Tab. '>
<b><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; font-weight:normal;'>
&nbsp;View/Copy Source Code&nbsp;</a></b>
</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>


<!-- 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 returns the decimal hours equivalent to the given HMS string.

   Generic. No special error checking is done.

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

   
function HMS_to_Hours ($HMSString$Decimals=16)
{
   
$HHmmss   trim($HMSString);
   
$decimals trim($Decimals);

/* ------------------------------------------------
   Account for and preserve any numerical +/- sign.
   Internal work will use absolute values and any
   numerical sign will be reattached the output.
*/
   
$NumSign substr($HHmmss,0,1);
   if (
$NumSign == '-')
      {
$HHmmss substr($HHmmss,1,StrLen($HHmmss));}
   else
      {
       if (
$NumSign == '+')
          {
$HHmmss substr($HHmmss,1,StrLen($HHmmss));}

       
$NumSign '+';
      }

// ------------------------------------------------------------------
// Replace any colons : with blank spaces and remove any white space.

   
$HHmmss PReg_Replace("/\s+/"" "Str_Replace(":"" "$HHmmss));

// ----------------------------------------
// Count the HMS time elements from 1 to 3.

   
$n  Substr_Count($HHmmss' ');

   
$hh $mm $ss 0;

/* ----------------------------------------------------------------------
   Collect all given time element values.  They can be integer or decimal
   values. Only counts up to three HMS values and any values beyond those
   are simply ignored.
*/
   
for ($i=0;   $i 1;   $i++)
  {
   if (
$n == 1){list($hh)         = PReg_Split("[ ]"$HHmmss);}
   if (
$n == 2){list($hh,$mm)     = PReg_Split("[ ]"$HHmmss);}
   if (
$n == 3){list($hh,$mm,$ss) = PReg_Split("[ ]"$HHmmss);}
  }

// ------------------------------------------------------------------------
// Compute HMS equivalent in decimal hours to the given number of decimals.

   
return $NumSign.(round((3600*$hh 60*$mm $ss)/3600,$decimals));

// End of  HMS_to_Hours(...)



/*
   ###########################################################################
   This function returns an HMS string equivalent to an hours argument rounded
   to the specified number of decimals.

   Generic. No special error checking is done.

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

   
function Hours_to_HMS ($Hours$Decimals=0)
{
   
$hours trim($Hours);  $NumSign = ($hours 0)? '-':'';
   
$hours Str_Replace('+'''Str_Replace('-'''$hours));

// ---------------------
// Set working decimals.

   
$Q 32;
   
$decimals floor(abs(trim($Decimals)));
   
$decimals = ($decimals $Q)? $Q $decimals;
   
$decimals = ($decimals <  0)?  $decimals;

// ------------------------------------
// Compute hours,minutes and seconds to
// the specified number of decimals.

   
$hh  bcAdd($hours'0');
   
$min bcMul('60'bcSub($hours$hh$Q),$Q);
   
$mm  bcAdd($min'0');
   
$sec bcMul('60'bcSub($min$mm$Q),$Q);
   
$ss  SPrintF("%1.$decimals"."f"$sec);
          if (
$ss 10){$ss "0$ss";}

// -------------------------------------------
// Try to account for that blasted 60s glitch.

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

// ------------------------------------------
// Construct and return time elements string.

   
$hh SPrintF("%02d"$hh);
   
$mm SPrintF("%02d"$mm);
   
$ss SPrintf("%1.$decimals"."f"$ss);
         if (
$ss 10){$ss "0$ss";}

   return 
"$NumSign$hh:$mm:$ss";

// End of  Hours_to_HMS (...)









/*
   ###########################################################################
   This function returns a simple apparent geocentric Voyager spacecraft
   positional ephemeris from the NASA/JPL Horizons API.

   If no ephemeris is returned, then the text from the API is returned as-is
   because it could be an error message or some other status or query text.

   NO DEPENDENCIES
   ###########################################################################
*/
   
function Voyager_G ($TargObjID,$StartDateTime,$StopDateTime)
{

// ===========================================================================
// Construct query URL for the NASA/JPL Horizons API.

   
$TargObjID trim($TargObjID);
   
$Command   URLEncode($TargObjID);

// --------------------
// Construct 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'"           .
   
"&RANGE_UNITS='AU'"            .
   
"&SUPPRESS_RANGE_RATE='NO'"    .
   
"&ANG_FORMAT='HMS'"            .
   
"&APPARENT='AIRLESS'"          .
   
"&CENTER='500@399'"            .
   
"&TIME_DIGITS='MINUTES'"       .
   
"&TIME_ZONE='+00:00'"          .
   
"&START_TIME='$StartDateTime'" .
   
"&STOP_TIME='$StopDateTime'"   .
   
"&STEP_SIZE='1 hour'"          .
   
"&EXTRA_PREC='NO'"             .
   
"&CSV_FORMAT='YES'"            .
   
"&QUANTITIES='2,20,21'"        ;
// ===========================================



/* ---------------------------------------------------------------------
   This function returns a simple apparent geocentric Voyager spacecraft
   ephemeris statistics from the NASA/JPL Horizons API.
*/
   
$EphemTable Str_Replace(",\n"" \n"File_Get_Contents($From_Horizons_API));

/* --------------------------------------------------------
   If no ephemeris is found,  then return the text from the
   API as-is. It may be an error message or some other text.
*/
   
if (StrPos($EphemTable'$$SOE') === FALSE) {return HTMLEntities($EphemTable);}

/* -----------------------------------------------------
   Set pointers to start and end of CSV ephemeris table.
   A value of FALSE means no ephemeris was found within
   the given source text.
*/
   
$i StrPos($EphemTable'$$SOE');
   
$j StrPos($EphemTable'$$EOE');

/* ------------------------------------------------
   Extract ONLY the existing ephemeris data line(s)
   from between the pointers.
*/
   
$EphemTable ' ' trim((substr($EphemTable$i+5$j-$i-5)));

// -----------------------------------
// And then, parse the table contents.

   
$EphemTable Parse_Geocentric_Table ($EphemTable);

// Done
   
return HTMLEntities($EphemTable);

// END OF  Voyager_G (...)










/*
   ###########################################################################
   This function returns a simple apparent heliocentric Voyager spacecraft
   ephemeris statistics from the NASA/JPL Horizons API.

   If no ephemeris is returned, then the text from the API is returned as-is
   because it could be an error message or some other status or query text.

   NO DEPENDENCIES
   ###########################################################################
*/
   
function Voyager_H ($TargObjID,$StartDateTime,$StopDateTime)
{

// ===========================================================================
// Construct query URL for the NASA/JPL Horizons API.

   
$TargObjID trim($TargObjID);
   
$Command   URLEncode($TargObjID);

// --------------------
// Construct 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'"           .
   
"&RANGE_UNITS='AU'"            .
   
"&SUPPRESS_RANGE_RATE='NO'"    .
   
"&ANG_FORMAT='HMS'"            .
   
"&APPARENT='AIRLESS'"          .
   
"&CENTER='500@10'"             .
   
"&TIME_DIGITS='MINUTES'"       .
   
"&TIME_ZONE='+00:00'"          .
   
"&START_TIME='$StartDateTime'" .
   
"&STOP_TIME='$StopDateTime'"   .
   
"&STEP_SIZE='1 hour'"          .
   
"&EXTRA_PREC='NO'"             .
   
"&CSV_FORMAT='YES'"            .
   
"&QUANTITIES='2,20,21'"        ;


/* ------------------------------------------------------------------------
   Send query to Horizons API to obtain the apparent heliocentric ephemeris
   statistics for the given body ID as a plain-text CSV ephemeris table.
*/
   
$EphemTable Str_Replace(",\n"" \n"File_Get_Contents($From_Horizons_API));

/* --------------------------------------------------------
   If no ephemeris is found,  then return the text from the
   API as-is. It may be an error message or some other text.
*/
   
if (StrPos($EphemTable'$$SOE') === FALSE)
   {
    return 
HTMLEntities($EphemTable);
   }

/* -----------------------------------------------------
   Set pointers to start and end of CSV ephemeris table.
   A value of FALSE means no ephemeris was found within
   the given source text.
*/
   
$i StrPos($EphemTable'$$SOE');
   
$j StrPos($EphemTable'$$EOE');

/* ------------------------------------------------
   Extract ONLY the existing ephemeris data line(s)
   from between the pointers.
*/
   
$EphemTable ' ' trim((substr($EphemTable$i+5$j-$i-5)));

// -----------------------------------
// And then, parse the table contents.

   
$EphemTable Parse_Heliocentric_Table ($EphemTable);

// Done.
   
return HTMLEntities($EphemTable);

// END OF  Voyager_H (...)






/*
   This function parses and reformats the geocentric
   ephemeris data for display.
*/
   
function Parse_Geocentric_Table ($GeoTableTxt)
{
   
$T trim($GeoTableTxt);

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

   
$out '';

   for (
$i=0;   $i $wCount;   $i++)
  {
   
$CurrLine trim($wArray[$i]);

   list (
         
$DateTimeStr,
         
$w$w,
         
$RA$Decl$Delta$DelDot,
         
$LightTime
        
) = Preg_Split("[,]"$CurrLine);

   
$RA        trim($RA);
   
$Decl      trim($Decl);
   
$Delta     SPrintF("% 11.7f",  trim($Delta));
   
$DelDot    SPrintF("% +11.3f"trim($DelDot)*3600/1.609344);

   
$LightTime Hours_to_HMS(trim($LightTime/60), 2);

   
$xLightTime LightTime_HMS_to_LightTime_DHMS ($LightTime);

   
$out .= "$DateTimeStr | $Delta | $DelDot  | $xLightTime\n";
  }
   return 
$out;

// END OF  Parse_Geocentric_Table (...)




/*
   This function parses and reformats the heliocentric
   ephemeris data for display.
*/

   
function Parse_Heliocentric_Table ($HelioTableTxt)
{
   
$T trim($HelioTableTxt);

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

   
$out '';

   for (
$i=0;   $i $wCount;   $i++)
  {
   
$CurrLine trim($wArray[$i]);

   list (
         
$DateTimeStr,
         
$w$w,
         
$RA$Decl$Delta$DelDot,
         
$LightTime
        
) = Preg_Split("[,]"$CurrLine);

   
$Delta     SPrintF("%11.7f",   trim($Delta));
   
$DelDot    SPrintF("% +11.3f"trim($DelDot)*3600/1.609344);

   
$LightTime Hours_to_HMS(trim($LightTime/60), 2);

   
$xLightTime LightTime_HMS_to_LightTime_DHMS ($LightTime);

   
$out .= "$DateTimeStr | $Delta | $DelDot  | $xLightTime\n";
  }
   return 
$out;

// END OF  Parse_Heliocentric_Table (...)



// ---------------------------------------------------------
// Utility function to add days to light time, if necessary.

   
function LightTime_HMS_to_LightTime_DHMS ($LightTimeHMS)
{
   
$HMS trim($LightTimeHMS);
          if (
IntVal($HMS) < 24) {return "0d $HMS";}

   list (
$HH,$mm,$ss) = PReg_Split("[:]"$HMS);

   
$days IntVal($HH 24);

   
$hh SPrintF("%02d"IntVal($HH) - 24);
   while (
$hh 24) {$hh SPrintF("%02d"IntVal($hh) - 24);}

   return 
"$days"d $hh:$mm:$ss";
}


// END OF PROGRAM




?>