<?php

/*
   ###########################################################################
   YEARLY SOLAR SYSTEM PERIGEE AND APOGEE FINDER - DEMO
   WITH 30-DAY COOKIE

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

   This program simply finds the local calendar dates of geocentric perigees
   and apogees of the sun, moon, planets and asteroids for any given year in
   the time-span of the ephemeris for the selected Body ID.   Not all bodies
   span thousands of years, but some major bodies, like the sun and moon, do.

   Depending on the Body, the span of the program is from year 0001 to 9998.
   This version does NOT handle BC years or compute the daily event times,
   but only the calendar dates according to the Time Scale and Time Zone.

   The program can also find  the perihelion and aphelion dates for the Earth
   by using the perigee and apogee of the sun as equivalent substitute values.

   It can also be used as a template to build upon and add new features.

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

   
ob_start(); // Oy!

   
$cYear GMDate('Y');

// -----------------------------------------------
// Un-comment to suppress the display of warnings.
// Error_Reporting(E_ERROR | E_PARSE);

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

   
$CookieName 'Yearly-Solar-System-Perigees-Apogees-Finder';
   
$ExpiresIn30Days 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 seconds.';";

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

   
$_AUTHOR_           "Jay Tanner of Geneva, NY, USA - $cYear";
   
$_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_ "Yearly Solar System Perigees and Apogees Finder";
   
$_INTERFACE_TITLE_  "<span style='font-size:15pt;'>Yearly Solar System Perigees and Apogees Finder</span><br><span style='font-size:11pt;'>Powered By The NASA/JPL Horizons API</span><br><span style='font-size:8.5pt;'>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 number of days in each month
   for use with calendar computations.
*/
   
define('MONTHDAYS''312831303130313130313031');

/* ----------------------------------------
   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 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.
*/
   
$w Filter_Input(INPUT_COOKIE$CookieName);

   if (IsSet(
$w))
      {
       
$CookieDataString Filter_Input(INPUT_COOKIE$CookieName);
       list
      (
       
$BodyID,
       
$TimeScale,
       
$StartYear,
       
$TimeZone,
       
$StepSize
      
) = 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.
*/

 
{
  
$BodyID     '301';
  
$TimeScale  'UT';
  
$StartYear  date('Y');
  
$TimeZone   '+00:00'// +Positive = East
  
$StepSize   '1 day';

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

   
$CookieDataString "$BodyID|$TimeScale|$StartYear|$TimeZone|$StepSize";
   
SetCookie ($CookieName$CookieDataString$ExpiresIn30Days);
  } 
// 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))
{
   
$BodyID     trim(Filter_Input(INPUT_POST'BodyID'));
                 if (
$BodyID == '') {$BodyID '10';}

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

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


   
$TimeZone  trim(Filter_Input(INPUT_POST'TimeZone'));
                if (
$TimeZone == '') {$TimeZone '+00:00';}
   
$TZHours   = @HMS_to_Hours($TimeZone); // suppress a harmless warning.
   
$TimeZone  Hours_to_HMS($TZHours);
   
$TZSign    = ($TZHours >= 0)? '+':'';
   
$TimeZone  substr($TZSign.$TimeZone,0,6);

   
$StepSize  '1 day';


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

   
$CookieDataString "$BodyID|$TimeScale|$StartYear|$TimeZone|$StepSize";
   
SetCookie ($CookieName$CookieDataString$ExpiresIn30Days);
}


/* -----------------------------------------------------------
   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 '780';


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


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


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

   
$StopYear         SPrintF("%04d"$StartYear 1);
   
$OutputTextHeader "CalendarDate_HR:MN, , ,            delta,     deldot";
   
$StartDateTime    "$StartYear-Jan-01  00:00:00 $TimeScale";
   
$StopDateTime     "$StopYear-Jan-01  00:00:00";

// ------------------------
// Define special messages.

   
$TZMssg $TimeZone;
   if (
$TimeScale == 'TT')
      {
       
$TZMssg "Time Zone is Ignored";
      }

   
$xTZMssg = ($TimeScale == 'TT')? $TZMssg "UT$TZMssg";


   
$TimeMssg '';
   if (
$TimeScale == 'TT')
      {
$TimeMssg "--------------------\nDates are TT\n";}

   if (
$TimeScale == 'UT' and $TimeZone == '+00:00')
      {
$TimeMssg "--------------------\nDates are UT\n";}

   if (
$TimeScale == 'UT' and $TimeZone <> '+00:00')
      {
$TimeMssg "--------------------------\nLocal Standard Times\n";}


// ---------------------------------------------------
// Determine if perigee/perihelion or apogee/aphelion.

   
$AphMssg "Geocentric Perigee and Apogee Dates For the Year $StartYear\nvia The NASA/JPL Horizons API";
   if (
$BodyID == 10)
      {
       
$AphMssg Str_Replace('Perigee''Earth Perihelion'$AphMssg);
       
$AphMssg Str_Replace('Apogee',  'Aphelion',   $AphMssg);
      }

// ----------
// DEMO CALLS

/*-
   $RawPeriApoTable  = Raw_Peri_Apo_Table ($BodyID, $StartDateTime, $StopDateTime);
   $PeriApoDatePairs = Get_Peri_Apo_Date_Pairs($RawPeriApoTable);
*/

   
$PADates Perigee_and_Apogee_Dates ($BodyID$StartDateTime$StopDateTime);

   if (
$PADates == '')
      {
$TimeMssg "No events found for the calendar year $StartYear in time zone UT$TimeZone";}


   if (
$BodyID == 10)
  {
   
$PADates Str_Replace('Perigee''Perihelion'$PADates);
   
$PADates Str_Replace('Apogee',  'Aphelion',   $PADates);
  }
   
$BodyDesig Str_Replace("{""\n"$BodyDesig);
   
$BodyDesig Str_Replace("}""",   $BodyDesig);
   
$BodyDesig Str_Replace("source:""Source:"$BodyDesig);




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

   
$TextArea1Text =
"##############################################################################
$AphMssg

--------------------------
For Solar System Body ID:
$BodyID
$BodyDesig

Base Time Scale = 
$TimeScale
Local Time Zone = 
$xTZMssg

$TimeMssg
$PADates
--------------------------
"
;
  }


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

   
$TextArea2Text =
"PROGRAM INFO:

For the dates of Earth perihelion and aphelion, use Body ID = 10 for solar
perigee and apogee, which equate to the same values.

Sun Perigee = Earth Perihelion
Sun Apogee  = Earth Aphelion

If Body ID = 399 = Earth, it will return an error.

Asteroid (small bodies) should have a semicolon after the number ID.
EXAMPLES: Body ID = 301    = Our Moon (Luna) = Planet #3, Moon #01
          Body ID = 301;   = Asteroid #301   = '301 Bavaria (A890 WA)'
          Body ID = 4;     = Asteroid #4     = '4 Vesta (A807 FA)'
          Body ID = 123;   = Asteroid #123   = '123 Brunhild (A872 OB)'
          Body ID = 1843;  = Asteroid #1843  = '1843 Jarmila (1972 AB)'
          etc. ...


TIME SCALES: There are three time scales available, TT, UT and local time
             as dictated by the local standard time zone offset from UT.
"
;



/* --------------------------------------------------------------------------
   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 80) {$Text1Cols 80;}
   
$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;}
   
$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='expires'      content='-1'>
<meta http-equiv='pragma'       content='no-cache'>
<meta name='description'        content='Yearly-Perigee/Apogee Search'>
<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 input (BodyID) text box  --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td style='line-height:175%; background:LightCyan;' width='25%'
title='&nbsp;MAJOR BODY  IDs&nbsp;
10   = Sun
199 = Mercury
299 = Venus
301 = Moon (Luna)&nbsp;
399 = Earth
499 = Mars
599 = Jupiter
699 = Saturn
799 = Uranus
899 = Neptune
999 = Pluto

&nbsp;ASTEROID  IDs
1;    = Ceres;
2;    = Pallas;
3;    = Juno;
4;    = Vesta;
5;    = Astraea;
6;    = Hebe;
7;    = Iris;
8;    = Flora;
9;    = Metis;
10;  = Hygiea;

-31  = Voyager 1
-32  = Voyager 2
'>NASA/JPL&nbsp;Database&nbsp;Body&nbsp;ID<br>
<input name="BodyID"  type="text" value="
$BodyID" size="33" maxlength="32"></td>
</tr>
</table>

<!-- Define input (TimeScale) text box  --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td style='line-height:175%; background:LightYellow;' width='33%' title=' UT = Universal Time (Default)\n TT = Terrestrial (Dynamical) Time '>Base&nbsp;Time&nbsp;Scale<br><input name="TimeScale"  type="text" value="
$TimeScale" size="3" maxlength="2"></td>

<td width='33%' title=' A.D. Year Span: 0001 to 9998 \n\n NOTE:\n Not all objects have the same \n ephemeris span. '>A.D.&nbsp;Year<br>
<input name="StartYear"  type="text" value="
$StartYear" size="6" maxlength="5">
<td width='33%'>
Time Zone Offset<br>
<b>UT</b>&nbsp;<input name="TimeZone"  type="text" value="
$TimeZone" size="7" maxlength="6"></td>

</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 (...)








   
function Raw_Peri_Apo_Table ($BodyID$StartDateTime$StopDateTime)
{
   GLOBAL 
$TimeZone$BodyDesig;

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

   
$BodyID  trim($BodyID);
   
$Command URLEncode($BodyID);

   
$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='$TimeZone'"       .
   
"&START_TIME='$StartDateTime'" .
   
"&STOP_TIME='$StopDateTime'"   .
   
"&STEP_SIZE='1 day'"           .
   
"&EXTRA_PREC='YES'"            .
   
"&CSV_FORMAT='YES'"            .
   
"&QUANTITIES='20'"             ;
// ===========================================

/* -----------------------------------------------------------------------
   Send query to Horizons API to obtain the apparent geocentric ephemeris
   data for the given body ID as a plain-text CSV ephemeris table.
*/
   
$GeocentEphem 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($GeocentEphem'$$SOE') === FALSE) {return $GeocentEphem;}
  {

// ------------------------------------------------------
// Get target body designation and source info substring.

   
$i StrPos($GeocentEphem'Target body name:');
   
$j StrPos($GeocentEphem'}');
        
$BodyDesig trim(substr($GeocentEphem$i+17$j-$i-16));
        
$BodyDesig PReg_Replace("/\s+/"" "trim($BodyDesig));

/* -----------------------------------------------------
   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($GeocentEphem'$$SOE');
   
$j StrPos($GeocentEphem'$$EOE');

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

  
$w HTMLEntities(trim($GeocentEphem));

  return (
substr($w,0,1) == 'b')? $w $w";


// END OF  Raw_Perigee_Apogee_Table (...)

















   
function Get_Peri_Apo_Date_Pairs ($RawPeriApoTable)
{
   
$RawPATable trim($RawPeriApoTable);

   
$wArray PReg_Split("[\n]"$RawPATable);
   
$wCount count($wArray);
   
$output $PorA '';

// -------------------------------------------
// Process lines in pairs from index 1 to end.
//  2026-Jan-01 00:00, , , 0.00241343753005, -0.0170778
   
for($i=1;   $i $wCount;   $i++)
  {
   
$PrevLine trim($wArray[$i-1]);
   
$CurrLine trim($wArray[$i-0]);


   list
  (
   
$PrevDate,
   
$w,$w,$w,
   
$PrevDelDot
  
) = PReg_Split("[,]" $PrevLine);

   list
  (
   
$CurrDate,
   
$w,$w,$w,
   
$CurrDelDot
  
) = PReg_Split("[,]" $CurrLine);

   if (
substr($PrevDate,0,1) == 'b')
      {
$PrevDate 'BC ' substr($PrevDate,1,StrLen($PrevDate));}
   else
      {
$PrevDate 'AD ' $PrevDate;}

   if (
substr($CurrDate,0,1) == 'b')
      {
$CurrDate 'BC ' substr($CurrDate,1,StrLen($CurrDate));}
   else
      {
$CurrDate 'AD ' $CurrDate;}

   
$PrevDelDot trim($PrevDelDot);
                 if  (
$PrevDelDot 0) {$PrevDelDot "+$PrevDelDot";}
   
$CurrDelDot trim($CurrDelDot);
                 if  (
$CurrDelDot 0) {$CurrDelDot "+$CurrDelDot";}

   if (
$PrevDelDot and $CurrDelDot 0)
      {
       
$output .= "$PrevDate, P\n$CurrDate\n*\n";
      }
   if (
$PrevDelDot and $CurrDelDot 0)
      {
       
$output .= "$PrevDate, A\n$CurrDate\n*\n";
      }
  }
   
$output Str_Replace(' 00:00',''$output);

   return 
RTrim($output"\n*");
}











/*
   This function returns a table of perigee and apogee
   calendar dates for the sun, moon, planets and even
   asteroids.
*/

   
function Get_Peri_Apo_Dates ($RawPeriApoTable)
{
   
$RawPATable trim($RawPeriApoTable);

   
$wArray PReg_Split("[\n]"$RawPATable);
   
$wCount count($wArray);
   
$output $PorA '';

// -------------------------------------------
// Process lines in pairs from index 1 to end.
//  2026-Jan-01 00:00, , , 0.00241343753005, -0.0170778
   
for($i=1;   $i $wCount;   $i++)
  {
   
$PrevLine trim($wArray[$i-1]);
   
$CurrLine trim($wArray[$i-0]);

   list
  (
   
$PrevDate,
   
$w,$w,$w,
   
$PrevDelDot
  
) = PReg_Split("[,]" $PrevLine);

   list
  (
   
$CurrDate,
   
$w,$w,$w,
   
$CurrDelDot
  
) = PReg_Split("[,]" $CurrLine);

   if (
substr($PrevDate,0,1) == 'b')
      {
$PrevDate 'BC ' substr($PrevDate,1,StrLen($PrevDate));}
   else
      {
$PrevDate 'AD ' $PrevDate;}

   if (
substr($CurrDate,0,1) == 'b')
      {
$CurrDate 'BC ' substr($CurrDate,1,StrLen($CurrDate));}
   else
      {
$CurrDate 'AD ' $CurrDate;}


   if (
$PrevDelDot and $CurrDelDot 0)
      {
       
$output .= "$PrevDate, P\n";
      }
   if (
$PrevDelDot and $CurrDelDot 0)
      {
       
$output .= "$PrevDate, A\n";
      }
  }
   
$output Str_Replace(' 00:00',''$output);

   
$output Str_Replace("P\n","Perigee\n"$output);
   
$output Str_Replace("A\n","Apogee\n"$output);

   return 
RTrim($output"\n*");
}






   function 
Perigee_and_Apogee_Dates ($BodyID$StartDateTime$StopDateTime)
{
   GLOBAL 
$TimeZone$BodyDesig;

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

   
$BodyID  trim($BodyID);
   
$Command URLEncode($BodyID);

   
$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='$TimeZone'"       .
   
"&START_TIME='$StartDateTime'" .
   
"&STOP_TIME='$StopDateTime'"   .
   
"&STEP_SIZE='1 day'"           .
   
"&EXTRA_PREC='YES'"            .
   
"&CSV_FORMAT='YES'"            .
   
"&QUANTITIES='20'"             ;
// ===========================================

/* -----------------------------------------------------------------------
   Send query to Horizons API to obtain the apparent geocentric ephemeris
   data for the given body ID as a plain-text CSV ephemeris table.
*/

   
$RawPeriApoTable File_Get_Contents($From_Horizons_API)."\n";

/* --------------------------------------------------------
   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($RawPeriApoTable'$$SOE') === FALSE)
      {
       
$RawPeriApoTable Str_Replace('Observer table for observer=target disallowed.','ERROR: Cannot use 399 (Earth itself) as a target body.'$RawPeriApoTable);
       return 
$RawPeriApoTable;
      }
  {

// ------------------------------------------------------
// Get target body designation and source info substring.

   
$i StrPos($RawPeriApoTable'Target body name:');
   
$j StrPos($RawPeriApoTable'}');
        
$BodyDesig trim(substr($RawPeriApoTable$i+17$j-$i-16));
        
$BodyDesig PReg_Replace("/\s+/"" "trim($BodyDesig));

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

/* ------------------------------------------------
   Extract ONLY the existing ephemeris data line(s)
   from between the pointers.
*/
   
$RawPeriApoTable RTrim(substr($RawPeriApoTable$i+5$j-$i-5));
  }
   
$w HTMLEntities(trim($RawPeriApoTable));
   
$w = (substr($w,0,1) == 'b')? $w $w";

   
$RawPATable trim($w);

   
$wArray PReg_Split("[\n]"$RawPATable);
   
$wCount count($wArray);
   
$output '';

// -------------------------------------------
// Process lines in pairs from index 1 to end
// and extract sub-table of P/A dates.

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

     list
    (
     
$PrevDate,
     
$w,$w,$w,
     
$PrevDelDot
    
) = PReg_Split("[,]" $PrevLine);

     list
    (
     
$CurrDate,
     
$w,$w,$w,
     
$CurrDelDot
    
) = PReg_Split("[,]" $CurrLine);

// ----------------------------------------------------
// Mark dates with 'P' for Perigee  or  'A' for Apogee.
// Could also mean Perihelion or Aphelion respectively.

     
if ($PrevDelDot and $CurrDelDot 0)
        {
         
$output .= "$PrevDate  P\n"// Append Perigee/Perihelion mark.
        
}
     if (
$PrevDelDot and $CurrDelDot 0)
        {
         
$output .= "$PrevDate  A\n"// Append Apogee/Aphelion mark.
        
}
    }

// --------------------------
// Do some output formatting.

   
$output Str_Replace(' 00:00''',       $output);
   
$output Str_Replace("P\n""Perigee\n"$output);
   
$output Str_Replace("A\n""Apogee\n",  $output);

// Done.
   
return trim($output);

// END OF  Perigee_and_Apogee_Dates()







// END OF PROGRAM




?>