<?php

/*
   THIS IS A CUSTOM JPL HORIZONS API EPHEMERIS INTERFACE - WITH 30-DAY COOKIE.
   BUILT AROUND THE NASA/JPL HORIZONS API.

   ---------------------------------------------------------------------------
   This program can produce more than one type of custom ephemeris, depending
   on the given settings. The three most common types are:

   Apparent Local Topocentric (Default)
   Apparent Astrometric (Both ICRF and B1950/FK4 systems)
   Apparent Geocentric (ICRF)

   ###########################################################################
   The ICRF (International Celestial Reference Frame) is the default frame,
   but the B1950/FK4 frame can be used for older astrometric coordinates.

   The refraction setting is ignored for astrometric coordinates or
   geocentric coordinates.

   Selecting the TT time scale will force the Time Zone setting to 'No',
   since it is ignored and does not apply to that scale anyway.

   The Time Zone setting is only applied to the UT Time Scale.  When the
   Time Scale is UT and the Time Zone is +00:00, then UT is assumed.
   Otherwise, local ZONE Time is assumed.

   When Daylight/Summer Time is selected, +1 hour is applied to the Time
   Zone internally.

   AUTHOR   : Jay Tanner - 2025
   LANGUAGE : PHP v8.2.12

   LICENSE  : Public Domain
              You are free to change the program in any way you wish to
              expand upon or customize it.

   NOTE:
   All ephemerides are plain-text tables in CSV (spreadsheet) format to
   facilitate easy column parsing.

   Ref:
   https://ssd.jpl.nasa.gov/horizons/app.html
   See: [Table Settings] for full quantities list.

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


/* ----------------------------------------------------------
   OB was the first caveman and LONG was his life. (33 years)
*/
   
ob_start();


/* --------------------------------
   Define the program cookie name
   and set it to expire in 30 days.
*/
   
$CookieName 'NASA-JPL-Horizons-Ephemeris-Tool';
   
$SetToExpireIn30Days time() + 30*86400;


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

   
$_AUTHOR_  'by Jay Tanner';

   
$_VERSION_ '2025-Apr-23-Wed';
         
$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_    $_VERSION_ 'Revised: '
                       
date("Y-M-d-D $at h:i:s A   ($LTC"FileMTime($_SCRIPT_FILE_PATH_))
                       . 
"&minus;05:00)";
   
$_BROWSER_TAB_TEXT_ "NASA/JPL General Horizons Ephemeris Tool";
   
$_INTERFACE_TITLE_  "<span style='font-size:14.5pt;'>EXPERIMENTAL<br><br>NASA/JPL&nbspHorizons&nbsp;General&nbsp;Ephemeris&nbsp;Tool</span>
   <span style='font-size:12pt;'>+</span>
   <span style='font-size:12pt;'>Data&nbsp;Query&nbsp;Portal</span><br>
   <span style='font-size:11pt;'>Built&nbsp;around&nbsp;the&nbsp;NASA/JPL&nbsp;Horizons&nbsp;API</span><br>
   <span style='font-size:10pt;'>
$_VERSION_ $_AUTHOR_</span>";


// ---------------------------------------------------
// Define JavaScript message to display while working.

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


// -------------------------------------------
// DEFINE DEFAULT HOME LOCATION AND TIME ZONE.

   
$DefaultLoc 'Central New York State, USA';
   
$DefaultLon '-76.862737';
   
$DefaultLat '+42.904788';
   
$DefaultTZ  '-05:00';

   
$DefaultGeocentYN  'Yes';     // 'Yes|No'
   
$DefaultQuantities '1,20,9';  // (1=Astrometric) RA, Decl, Dist, RadVel

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


/* -----------------------------------------------
   Define text for HTML title-text INFO blocks.
   Just hover over the 'INFO' text and a yellow
   menu will be displayed.
*/
   
$TargObjIDTitleText =
" This can be any NASA/JPL Object ID or Special Query.
QUERY   RETURNS
=====   ==============================================
News    Horizons System News + News Backlog
?       Basic information on using the Horizons API
?!      Technical information behind the API
*       List of Major Bodies and IDs or Record #s
MB      Same as *
-*      List of Spacecraft, Vehicles, Rovers, etc.
;*      List of Bodies and Record #s (very long list)
com;    List of Comets and Record #s (very long list)
"
;


// ----------------------------------------------------
// Read [SUBMIT] button state.

   
$w Filter_Input(INPUT_POST'SubmitButton');

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

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

// Try to read cookie content, if any.
   
$Cookie Filter_Input(INPUT_COOKIE$CookieName);

// Check if cookie content exists.
// IsSet === Has content === Not empty

   
if (IsSet($Cookie))
      {
       
$CookieDataString Filter_Input(INPUT_COOKIE$CookieName);
       list
      (
       
$TargObjID,
       
$TimeScale,
       
$TimeZone,
       
$StartBCAD,
       
$StartYear,
       
$StartMonth,
       
$StartDay,
       
$StartTime,
       
$StopBCAD,
       
$StopYear,
       
$StopMonth,
       
$StopDay,
       
$StopTime,
       
$StepSize,
       
$LocName,
       
$LonDeg,
       
$LatDeg,
       
$AltMet,
       
$DaySumYN,
       
$RefractYN,
       
$DEGorHMS,
       
$ObjDataYN,
       
$SuppRangeRateYN,
       
$JDateYN,
       
$AUorKM,
       
$EphemHeaderYN,
       
$EphemFooterYN,
       
$RefSystem,
       
$GeocentYN,
       
$Quantities
      
) = 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.
*/

 
{
   
$TargObjID  '10'// Sun = Default
   
$TimeScale  'UT'// or 'TT'
   
$TimeZone   '+00:00';
   
$StartBCAD  'AD';
   
$StartYear  date('Y');
   
$StartMonth date('M');
   
$StartDay   date('d'); // Current day of the month.
   
$StartTime  date('00:00:00');

   
$StopBCAD   'AD';
   
$StopYear   date('Y');
   
$StopMonth  date('M');
   
$StopDay    Days_In_Month("$StartBCAD $StartYear-$StartMonth-01"); // End of month.
   
$StopTime   date('00:00:01');
   
$StepSize   '1 day';

   
$LocName '---';
   
$LonDeg  '0';
   
$LatDeg  '0';
   
$AltMet  '+0';

   
$DaySumYN        'No';   // 'Yes|No'
   
$RefractYN       'No';   // 'Yes|No'
   
$DEGorHMS        'HMS';  // 'DEG|HMS'
   
$ObjDataYN       'No';   // 'Yes|No'
   
$SuppRangeRateYN 'No';   // 'Yes|No'
   
$JDateYN         'No';   // 'Yes|No'
   
$AUorKM          'AU';   // 'AU|KM'
   
$EphemHeaderYN   'No';   // 'Yes|No'
   
$EphemFooterYN   'No';   // 'Yes|No'
   
$RefSystem       'ICRF'// 'ICRF|B1950/FK4'
   
$GeocentYN       'Yes';
   
$Quantities      '1,20,9';

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

   
$CookieDataString "$TargObjID|$TimeScale|$TimeZone|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$StopBCAD|$StopYear|$StopMonth|$StopDay|$StopTime|$StepSize|$LocName|$LonDeg|$LatDeg|$AltMet|$DaySumYN|$RefractYN|$DEGorHMS|$ObjDataYN|$SuppRangeRateYN|$JDateYN|$AUorKM|$EphemHeaderYN|$EphemFooterYN|$RefSystem|$GeocentYN|$Quantities";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
  } 
// End of  else {...}

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

/* ------------------------------------------
   READ VALUES OF INTERFACE ARGUMENTS AND SET
   ANY EMPTY ARGUMENTS TO THE DEFAULT VALUES
   GIVEN HERE.
*/

// Read [SUBMIT] button state.
   
$SubmitButton Filter_Input(INPUT_POST'SubmitButton');

// Check [SUBMIT] button state.
// IsSet TRUE Means Clicked.
   
if (IsSet($SubmitButton))
{
// -----------------------------------------------
// Read arguments from interface input text boxes.

   
$TargObjID trim(Filter_Input(INPUT_POST'TargObjID'));
                 if (
$TargObjID == '') {$TargObjID '10';}

   
$StartBCAD  trim(Filter_Input(INPUT_POST'StartBCAD'));
   
$StartBCAD  = (substr(StrToUpper($StartBCAD),0,1) == 'B')? 'BC':'AD';

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



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

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

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



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

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

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

   
$StopBCAD trim(Filter_Input(INPUT_POST'StopBCAD'));
   
$StopBCAD = (substr(StrToUpper($StopBCAD),0,1) == 'B')? 'BC':'AD';

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



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

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



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

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

   
$StepSize  trim(Filter_Input(INPUT_POST'StepSize'));
                 if (
$StepSize == '') {$StepSize '1 day';}
   
$StepSize .= (Is_Numeric($StepSize))? ' day' '';

   
$DaySumYN trim(Filter_Input(INPUT_POST'DaySumYN'));
   
$DaySumYN = (substr(StrToUpper($DaySumYN),0,1) == 'Y')? 'Yes':'No';

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

// ----------------------------------------------------
// Get longitude input as decimal degrees or DMS string
// which will be converted into decimal degrees.

   
$LonDeg trim(Filter_Input(INPUT_POST'LonDeg'));
             if (
$LonDeg == '') {$LonDeg $DefaultLon;}
   
$LonDeg DMS_to_Deg($LonDeg9);


// ---------------------------------------------------
// Get latitude input as decimal degrees or DMS string
// which will be converted into decimal degrees.

   
$LatDeg trim(Filter_Input(INPUT_POST'LatDeg'));
             if (
$LatDeg == '') {$LatDeg $DefaultLat;}
   
$LatDeg DMS_to_Deg($LatDeg9);


   
$AltMet trim(Filter_Input(INPUT_POST'AltMet'));
             if (
$AltMet == '') {$AltMet '+0';}
   
$AltMet SPrintF("%+d"$AltMet);


   
$RefractYN trim(Filter_Input(INPUT_POST'RefractYN'));
                if (
$RefractYN == '') {$RefractYN 'No';}
   
$RefractYN = (substr(StrToUpper($RefractYN),0,1) == 'Y')? 'Yes':'No';



   
$DEGorHMS trim(Filter_Input(INPUT_POST'DEGorHMS'));
               if (
$DEGorHMS == '') {$DEGorHMS 'HMS';}
   
$DEGorHMS = (substr(StrToUpper($DEGorHMS),0,1) == 'D')? 'DEG':'HMS';


/* ---------------------------------------------------------------
   TOPOCENTRIC CUSTOMIZATION NOTE:
   In this TOPOCENTRIC  ephemeris, only ICRF coordinates are used.
   The B1950/FK4 frame only applies to astrometric ephemerides.
*/
   
$RefSystem trim(Filter_Input(INPUT_POST'RefSystem'));
                if (
$RefSystem == '') {$RefSystem 'ICRF';}
   
$RefSystem = (substr(StrToUpper($RefSystem),0,1) == 'B')? 'B1950':'ICRF';



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


   
$ObjDataYN trim(Filter_Input(INPUT_POST'ObjDataYN'));
                if (
$ObjDataYN == '') {$ObjDataYN 'No';}
   
$ObjDataYN = (StrToUpper(substr($ObjDataYN,0,1)) == 'Y')? 'Yes':'No';


   
$SuppRangeRateYN trim(Filter_Input(INPUT_POST'SuppRangeRateYN'));
                      if (
$SuppRangeRateYN == '') {$SuppRangeRateYN 'Yes';}
   
$SuppRangeRateYN = (StrToUpper(substr($SuppRangeRateYN,0,1)) == 'Y')? 'Yes':'No';



   
$JDateYN trim(Filter_Input(INPUT_POST'JDateYN'));
                      if (
$JDateYN == '') {$JDateYN 'Yes';}
   
$JDateYN = (StrToUpper(substr($JDateYN,0,1)) == 'Y')? 'Yes':'No';



   
$AUorKM trim(Filter_Input(INPUT_POST'AUorKM'));
   
$AUorKM = (StrToUpper(substr($AUorKM,0,1)) == 'K')? 'KM':'AU';



   
$EphemHeaderYN trim(Filter_Input(INPUT_POST'EphemHeaderYN'));
                    if (
$EphemHeaderYN == '') {$EphemHeaderYN 'No';}
   
$EphemHeaderYN = (StrToUpper(substr($EphemHeaderYN,0,1)) == 'Y')? 'Yes':'No';



   
$EphemFooterYN trim(Filter_Input(INPUT_POST'EphemFooterYN'));
                    if (
$EphemFooterYN == '') {$EphemFooterYN 'No';}
   
$EphemFooterYN = (StrToUpper(substr($EphemFooterYN,0,1)) == 'Y')? 'Yes':'No';


   
$RefSystem trim(Filter_Input(INPUT_POST'RefSystem'));
                if (
$RefSystem == '') {$RefSystem 'ICRF';}
   
$RefSystem = (StrToUpper(substr($RefSystem,0,1)) == 'B')? 'B1950':'ICRF';


   
$GeocentYN trim(Filter_Input(INPUT_POST'GeocentYN'));
                if (
$GeocentYN == '') {$GeocentYN 'Yes';}
   
$GeocentYN = (StrToUpper(substr($GeocentYN,0,1)) == 'Y')? 'Yes':'No';


   
$Quantities trim(Filter_Input(INPUT_POST'Quantities'));
                 if (
$Quantities == '') {$Quantities $DefaultQuantities;}
   
$Quantities Str_Replace(','' '$Quantities);
   
$Quantities PReg_Replace("/\s+/"" "trim($Quantities));
   
$Quantities Str_Replace(' '','$Quantities);
   
$Quantities LTrim(RTrim($Quantities','), ',');


   
$TimeScale trim(Filter_Input(INPUT_POST'TimeScale'));
                if (
$TimeScale == '') {$TimeScale 'UT';}


// ------------------------------------------------------------
// Patches to prevent Day=00 error crash by inserting defaults.

   
if ($StartDay == '00')  {$StartDay '01';}
       
$StartDate "$StartBCAD $StartYear-$StartMonth-$StartDay";

   if (
$StopDay == '00') {$StopDay Days_In_Month($StartDate);}
       
$StopDate "$StopBCAD $StopYear-$StopMonth-$StopDay";


// ################################################################
// ################################################################
// ################################################################
// ################################################################
// ################################################################


/* -----------------------------------------------------
   Entering 'DEMO' will set up these demo example values
   as used for the JPL HTTP example ephemeris query at:
   https://ssd-api.jpl.nasa.gov/doc/horizons.html
*/
      
$WXYZ trim(Filter_Input(INPUT_POST'WXYZ'));

   if (
StrToUpper($WXYZ) == 'DEMO')
      {
       
$TargObjID       '499';
       
$LocName         '---';
       
$TimeScale       'UT';

       
$StartBCAD       'AD';
       
$StartYear       '2006';
       
$StartMonth      'Jan';
       
$StartDay        '01';
       
$StartTime       '00:00:00';

       
$TimeZone        '+00:00';
       
$DaySumYN        'No';

       
$StopBCAD        'AD';
       
$StopYear        '2006';
       
$StopMonth       'Jan';
       
$StopDay         '20';
       
$StopTime        '00:00:00';

       
$StepSize        '1 day';

       
$LonDeg          '+0';
       
$LatDeg          '+0';
       
$AltMet          '+0';

       
$RefSystem       'ICRF';
       
$GeocentYN       'Yes';
       
$DEGorHMS        'HMS';
       
$RefractYN       'No';
       
$ObjDataYN       'Yes';
       
$JDateYN         'No';
       
$EphemHeaderYN   'Yes';
       
$EphemFooterYN   'Yes';
       
$AUorKM          'AU';
       
$SuppRangeRateYN 'No';

       
$Quantities      '1,9,20,23,24,29';
     }




/* --------------------
   SPECIAL RESET PATCH.

   Entering 'RESET' will reset ALL interface
   values to their initial default values.
*/
   
if (StrToUpper($WXYZ) == 'RESET')
      {
       
$TargObjID      '10';
       
$LocName         '---';
       
$TimeScale       'UT';
       
$StartBCAD       'AD';
       
$StartYear       date('Y');
       
$StartMonth      date('M');
       
$StartDay        date('d');
       
$StartTime       '00:00:00';
       
$TimeZone        '+00:00';
       
$DaySumYN        'No';
       
$StopBCAD        'AD';
       
$StopYear        date('Y');
       
$StopMonth       date('M');
       
$StopDay         Days_in_Month ("$StartBCAD $StartYear-$StartMonth-$StartDay");
       
$StopTime        '00:00:01';
       
$StepSize        '1 day';
       
$LonDeg          '0';
       
$LatDeg          '0';
       
$AltMet          '+0';
       
$RefSystem       'ICRF';
       
$GeocentYN       'Yes';
       
$DEGorHMS        'HMS';
       
$RefractYN       'No';
       
$ObjDataYN       'No';
       
$JDateYN         'No';
       
$EphemHeaderYN   'No';
       
$EphemFooterYN   'No';
       
$AUorKM          'AU';
       
$SuppRangeRateYN 'No';
       
$Quantities      $DefaultQuantities;
      }



/* -------------------------------------------
   Entering 'NOW' will set to current date and
   time only. Current single point ephemeris.
*/
   
if (StrToUpper($WXYZ) == 'NOW')
      {
       
$StartBCAD       'AD';
       
$StartYear       date('Y');
       
$StartMonth      date('M');
       
$StartDay        date('d');
       
$StartTime       date('H:i:s');
       
$StopBCAD        'AD';
       
$StopYear        date('Y');
       
$StopMonth       date('M');
       
$StopDay         date('d');
       
$StopTime        date('H:i:59');
       
$StepSize        '1 day';
      }



/* -------------------
   SPECIAL HOME PATCH.

   Entering 'HOME' will default ALL interface
   values to their custom home values.
*/
   
if (StrToUpper($WXYZ) == 'HOME')
      {
       
$TargObjID      '10';
       
$LocName         $DefaultLoc;
       
$TimeScale       'UT';
       
$StartBCAD       'AD';
       
$StartYear       date('Y');
       
$StartMonth      date('M');
       
$StartDay        date('d');
       
$StartTime       '00:00:00';
       
$TimeZone        '-05:00';
       
$DaySumYN        'No';
       
$StopBCAD        'AD';
       
$StopYear        date('Y');
       
$StopMonth       date('M');
       
$StopDay         Days_in_Month ("$StartBCAD $StartYear-$StartMonth-$StartDay");
       
$StopTime        '00:00:01';
       
$StepSize        '1 day';
       
$LonDeg          $DefaultLon;
       
$LatDeg          $DefaultLat;
       
$AltMet          '+0';
       
$RefSystem       'ICRF';
       
$GeocentYN       'No';
       
$DEGorHMS        'HMS';
       
$RefractYN       'No';
       
$ObjDataYN       'No';
       
$JDateYN         'No';
       
$EphemHeaderYN   'No';
       
$EphemFooterYN   'No';
       
$AUorKM          'AU';
       
$SuppRangeRateYN 'Yes';
       
$Quantities      '2,20,9';
      }



   
$TimeScale = (substr(StrToUpper($TimeScale),0,1) == 'T')? 'TT':'UT';


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

   
$CookieDataString "$TargObjID|$TimeScale|$TimeZone|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$StopBCAD|$StopYear|$StopMonth|$StopDay|$StopTime|$StepSize|$LocName|$LonDeg|$LatDeg|$AltMet|$DaySumYN|$RefractYN|$DEGorHMS|$ObjDataYN|$SuppRangeRateYN|$JDateYN|$AUorKM|$EphemHeaderYN|$EphemFooterYN|$RefSystem|$GeocentYN|$Quantities";
   
SetCookie ($CookieName$CookieDataString$SetToExpireIn30Days);
}

// ---------------------------------------------------------
// Turn off Daylight/Summer Time mode if using the TT scale.

   
if ($TimeScale == 'TT') {$DaySumYN 'No';}



// ------------------------------------------------------------
// Construct full start/stop and date/time strings for the API.

   
$StartDateTime "$StartBCAD $StartYear-$StartMonth-$StartDay  $StartTime";
   
$StopDateTime  "$StopBCAD $StopYear-$StopMonth-$StopDay  $StopTime";

// ---------------------------------------
// Time Zone text string for UT mode only.

   
$TZoneText = ($TimeScale == 'UT')? "Local Time Zone Offset   = UT$TimeZone       +Positive = East":"";


// ----------------------------------------------------------
// Define a text separator bar of 80 (# or pound) characters.

   
$_BAR_ Str_Repeat('#'80);


// ---------------------------------------------------------
// Set initial uniform width for table alignments in pixels.

   
$TableWidth '836';

// --------------------------------------------------
// Determine the number of days in the default month.

   
$mDays Days_in_Month ("$StartBCAD $StartYear-$StartMonth-$StartDay");


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

// --------------------------------------------------------
// Intercept special for special '?!' query to avoid crash.

   
if ($TargObjID == '?!')
      {
       
$RawEphem Get_Horizons_Tech_Data();
      }
else


/* ---------------------------------------------------------------
   Call the general ephemeris function using the given parameters.
*/

  
{
   
$RawEphem General_Ephem ($TargObjID,$TimeScale,$TimeZone,
                              
$StartDateTime,$StopDateTime,$StepSize,$LonDeg,
                              
$LatDeg,$AltMet,$DaySumYN,$RefractYN,$DEGorHMS,
                              
$ObjDataYN,$SuppRangeRateYN,$JDateYN,$AUorKM,
                              
$RefSystem,$GeocentYN,$Quantities);
  }


// ----------------------------------------------------------------
// Check for a few specific errors and rephrase the error messages.
// NOTE:  Certain special characters are encoded as HTML entities.

// Invalid date error.
   
$RawEphem Str_Replace('Cannot interpret date. Type &quot;?!&quot; or try YYYY-Mon-Dy {HH:MM} format.''ERROR: Either the Start Date or Stop Date is invalid.'$RawEphem);

// Invalid step size units error.
   
$RawEphem Str_Replace('Unknown units specification -- re-enter'"ERROR: Unknown Step Size Units.\n'$StepSize'\nMust be a numerical value followed by a units symbol.\n\nValid units are:\nM = Minutes | H = Hours | D = Days | MO = Months | Y = Years\n\nNOT case-sensitive."$RawEphem);

// Invalid quantities error 1.
   
$RawEphem Str_Replace('Cannot read QUANTITIES. Type ? or ?! for explanation.'"ERROR: One or more of the quantity values requested is invalid or unknown."$RawEphem);

// Invalid quantities error 2.
   
$RawEphem Str_Replace('Unknown quantity requested. Type ? for info.'"ERROR: One or more of the quantity values requested is invalid or unknown."$RawEphem);

   
$EphemFooterText '';

   if (
$EphemFooterYN == 'Yes')
      {
       
$EphemFooterText Get_Column_Meanings ($RawEphem);
      }


/* ----------------------------------------------------------
   Get simple lunar phase angle and text description of phase
   for the interface start Date/Time.
*/
   
$LunarPhaseAng $LunarPhaseText $MoonBlock '';

   if (
$TargObjID == '301')
  {
   
$LunarPhaseAng  Lunar_Phase_Angle ($StartDateTime,$TimeZone,$DaySumYN);
   
$LunarPhaseAng  SPrintF("%1.4f"$LunarPhaseAng);
   
$LunarPhaseText Lunar_Phase_to_Text($LunarPhaseAng);

   
$MoonBlock =
"
-----------------------------------
MOON PHASE AT GIVEN START DATE/TIME

Lunar Phase Angle  = 
$LunarPhaseAng &deg;
General Appearance = 
$LunarPhaseText
"
;

  }

/* ------------------------------------------------------------
   CHECK IF OR NOT AN EPHEMERIS WAS RETURNED.  AN ERROR MESSAGE
   OR SOME OTHER KIND OF TEXT BLOCK MAY POSSIBLY BE RETURNED IF
   A BAD PARAMETER IS DETECTED OR A SPECIAL DIRECTIVE WAS SENT.

   All ephemerides begin with special key markers.

   $$SOE = Start Of Ephemeris
   and
   $$EOE = End of Ephemeris

   Any returned ephemeris CSV table body is found
   between those markers.
*/

   
if (
       
StrPos($RawEphem,"\$\$SOE\n") !== FALSE
   
and
       
StrPos($RawEphem,"\$\$EOE\n") !== FALSE
      
)

/* ----------------------------------------------
   If an ephemeris appears to have been returned,
   then start processsing it here.
*/
  
{
   
$TargetNameText Get_Target_Name($TargObjID);

   
$ObjDataText '';
   if (
$ObjDataYN == 'Yes')
      {
$ObjDataText Get_Obj_Data($TargObjID);}

   
$EphemHeaderText '';
   if (
$EphemHeaderYN == 'Yes')
      {
$EphemHeaderText Get_Ephem_Header ($RawEphem);}

   
$CSVLabelsText Get_CSV_Labels ($RawEphem);
   
$CSVBodyText Get_CSV_Body ($RawEphem);

// ---------------------------------
// Construct main output text block.

   
$OutputText =
"$ObjDataText
$EphemHeaderText$TargetNameText

$CSVLabelsText
$CSVBodyText

$EphemFooterText";



// ---------------------------------------
// Get target body ID name or designation.

   
$TargetBodyNameText Get_Target_Name ($TargObjID);


/* ########################################################
   AVAILABLE FUNCTIONS

   Get_Horizons_Tech_Data ()
   Special_Horizons_Query (Query)

   Get_Obj_Data           (TargObjID)
   Get_Target_Name        (TargObjID)
   Get_Ephem_Header       (RawEphem)
   Get_CSV_Labels         (RawEphem)
   Get_CSV_Body           (RawEphem)
   Get_Column_Meanings    (RawEphem)
   Split_Dates            (RawEphem)
   HMS_to_Hours           (HMSString, Decimals)
   Hours_to_HMS           (Hours, Decimals)
   DMS_to_Deg             (DMSString, Decimals)
   Compass_Symbol         (AzimDeg)
   Lunar_Phase_to_Text    (PhaseAngDeg)
   JD_Num                 (BCADDateStr, JGAMode)
   Inv_JD_Num             (JDNumber, JGAMode)
   Is_Valid_Date_Str      (BCADDateStr)
   Lunar_Phase_Angle      (DateTimeStr,TimeZone,DaySumYN)
   ########################################################
*/

  
}

else


// --------------------------------------------------------
// If an ephemeris WAS NOT returned, then start processsing
// the returned text here.

{
// ------------------------------
// Strip out unwanted characters.

   
$OutputText Str_Replace('`'''$RawEphem);

   
$TargetBodyNameText $TargObjID;
}






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


   
$TextArea1Text =
"$_BAR_
BASIC HORIZONS EPHEMERIS TOOL
Based on the NASA/JPL Horizons API

NASA/JPL OBJECT ID:
$TargetBodyNameText

------------------------------------------------------------
OBSERVER LOCATION / CALENDAR DATE / TIME / ZONE

Optional Location Name   = 
$LocName

Base Time Scale          = 
$TimeScale
$TZoneText
Daylight / Summer Time   = 
$DaySumYN
Start Date/Time          = 
$StartDateTime
Stop  Date/Time          = 
$StopDateTime
Ephemeris Step Size      = 
$StepSize

GPS Longitude            = 
$LonDeg&deg;    +Positive = East
GPS Latitude             = 
$LatDeg&deg;
Sea Level Altitude       = 
$AltMet m

Reference System         = 
$RefSystem
Use Geocentric Coords    = 
$GeocentYN
Angular Output Mode      = 
$DEGorHMS
Apply Refraction         = 
$RefractYN
Include Object Data      = 
$ObjDataYN
Include Julian Date      = 
$JDateYN
Include Ephem Header     = 
$EphemHeaderYN
Include Ephem Footer     = 
$EphemFooterYN
Distance (Range) Units   = 
$AUorKM
Supppress Range Rate     = 
$SuppRangeRateYN

Requested Quantities     = 
$Quantities
$MoonBlock
_LONG_BAR_
$OutputText
_LONG_BAR_"
;




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

   
$TextArea2Text =
"
...
"
;





/* --------------------------------------------------------------------------
   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;}  // Default
   
$Text1Rows Substr_Count($TextArea1Text"\n");

// ----------------------------------------------------------
// Define long separator bar to try to match TextArea1 width.

   
$_LONG_BAR_ Str_Repeat('#'$Text1Cols);
   
$TextArea1Text Str_Replace('_LONG_BAR_'$_LONG_BAR_$TextArea1Text);


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

<!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='xxxxxxxxxxxxxxx'>
<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
{
 background:black; color:white; font-family:Verdana; font-size:12pt;
 line-height:125%;
}



 TABLE
{font-family:Verdana;}



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



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

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

 LI
{font-family:Verdana; line-height:150%;}



 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:10.5pt;
 font-weight:bold; padding:4pt; white-space:pre; border-radius:8px;
 line-height:125%;
}


/* ------------------------------------------
   Styles applied to the input text boxes. */

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

 INPUT[type='text']
{
 background:white; color:black; font-family:monospace; font-size:11pt;
 font-weight:bold; text-align:center; box-shadow:1px 2px 2px #808080;
 border:2px solid black; border-radius:4px; padding:2px;
}

 INPUT[type='text']:focus
{
 font-family:monospace; background:white; box-shadow:1px 2px 2px #808080;
 font-size:11pt; border:2px solid blue; text-align:center; font-weight:bold;
 border-radius:4px; padding:2px;
}


/* ------------------------------------
   Styles applied to the SUBMIT button. */

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




/* -------------------------------------------
   Styles applied to hyper-links.

   Link states below MUST be defined in CSS in
   the following sequence to work correctly:

   :link,  :visited,  :hover,  :active
*/

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

 A:visited
{
 font-size:10pt; background:transparent; color:DodgerBlue; 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;
}


/* -----------------------------------------
   Style applied to HR (Horizontal Rule). */

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


/* Special styles to create a synthetic title
   attribute tag called 'title-text' which
   does not formally exist. */

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

[title-text]:after
{
 opacity:1.0;
 content:attr(title-text);
 text-align:left;
 left:-378%;
 background-color:yellow;
 color:black;
 font-family:monospace;
 font-size:10pt;
 font-weight:bold;
 line-height:150%;
 position:absolute;
 padding:1px 5px 2px 5px;
 white-space:pre;
 border:3px solid red;
 border-radius:8px;
 box-shadow:1px 1px 3px #222222;
 z-index:1;
 visibility:hidden;
}
[title-text] {position: relative;}


/* ----------------------------------------------------------------------
   These styles change the highlighting text colors to 'black/yellow'. */

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



<!-- Object ID input --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr><td style="background:LightYellow; line-height:175%;" colspan='99'>
<b>NASA/JPL Object ID#,&nbsp; Record #, Query or Special Directive</b><br>
<input name="TargObjID"  type="text" value="
$TargObjID"  size="50" maxlength="81"
title='MAJOR BODIES
10   = Sun
199 = Mercury
299 = Venus
301 = Moon (Luna)&nbsp;
399 = Earth
499 = Mars
599 = Jupiter
699 = Saturn
799 = Uranus
899 = Neptune
999 = Pluto

ASTEROIDS
1;    = Ceres;
2;    = Pallas;
3;    = Juno;
4;    = Vesta;
6;    = Hebe;
7;    = Iris;
8;    = Flora;
9;    = Metis;
10;  = Hygiea;
15;  = Eunomia;
16;  = Psyche;
52;  = Europa;
65;  = Cybele;
511; = Davida;
704; = Interamnia;&nbsp;'><br>
<span title-text="
$TargObjIDTitleText">&nbsp;<b>INFO</b>&nbsp;</span>
</td></tr>
</table>




<!-- Optional Location Name Setting Table --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr>
<td style='background:#E0FFE0;' title=' This can be any location name or label. \n'>Optional Location Name<br>
<input name="LocName"  type="text" value="
$LocName" size="51" maxlength="50"></td>
</tr>
</table>




<!-- START DATE/TIME/ZONE TABLE --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr>
<!-- Base Time Scale Setting --->
<td width='25%' style='background:#EFEFEF;'
title=' Base Time Scale

 UT = Universal Time (Default)&nbsp;
 Uses the Time Zone Setting.

 TT = Terrestrial Dynamical Time
 Ignores the Time Zone Setting and
 the Daylight/Summer Time Setting. '>

Base Time Scale<br>
<input name='TimeScale' type='text' value="
$TimeScale" size='3' maxlength='2'>
</td>

<!-- START Date Setting --->
<td style="text-align:center; line-height:175%; background:LightCyan;" title=" This is the ephemeris Start Date and  Time. \n\n">
<b>START:</b> Calendar Date and Time<br>
<input name="StartBCAD"  type="text" value="
$StartBCAD"  size="3" maxlength="2"><input name="StartYear"  type="text" value="$StartYear"  size="5" maxlength="4"><input name="StartMonth" type="text" value="$StartMonth" size="4" maxlength="3"><input name="StartDay"   type="text" value="$StartDay"   size="3" maxlength="2">&nbsp;&nbsp;<input name="StartTime"  type="text" value="$StartTime"  size="9" maxlength="8" title=' Enter START Time as  HH : mm : ss '>
</td>

<td width='25%' style='background:#EFEFEF;'
title=
" Enter Time Zone offset from UT as  &plusmn;HH : mm

+Positive Time Zone = East
This setting is ignored if using the TT scale. ">
Time Zone<br>
<input name="TimeZone" type="text" value="
$TimeZone" size="7" maxlength="6" style='text-align:center;'>
</td>
</tr>
</table>


<!-- STOP DATE/TIME/STEP SIZE TABLE --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr title=
" This is the ephemeris Stop Date and Time&nbsp;

 For an ephemeris table, the Stop Date/Time must be&nbsp;
 LATER than the Start Date/Time.

 For a SINGLE Date/Time, set the Step Size to beyond&nbsp;
 the Stop Date/Time so that only the Start Date/Time&nbsp;
 will be used. ">

<!-- Daylight/Summer Time Setting --->
 <td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No = Use Standard Time (Default) \n This setting is ignored if using the TT scale. \n\n Yes = Use Daylight/Summer Time '>
 Daylight/Summer Time<br>
 <input name='DaySumYN' type='text' value="
$DaySumYN" size='4' maxlength='3'>
 </td>
</td>

<!-- STOP Date/Time Setting --->
<td width='50%' style="background:LightCyan; text-align:center; line-height:180%;">
<b>STOP:</b> Calendar Date and Time<br>
<input name="StopBCAD"  type="text" value="
$StopBCAD"  size="3" maxlength="2"><input name="StopYear"  type="text" value="$StopYear"  size="5" maxlength="4"><input name="StopMonth" type="text" value="$StopMonth" size="4" maxlength="3"><input name="StopDay"   type="text" value="$StopDay"   size="3" maxlength="2">&nbsp;&nbsp;<input name="StopTime"  type="text" value="$StopTime"  size="9" maxlength="12" title=' Enter STOP Time as  HH : mm : ss '>
</td>



<!-- Ephemeris Step Size Setting --->
<td width='25%'title=' This is the time interval between the tabulated ephemeris computations.\n
 Step Units :  M = Minutes | H = Hours | D = Days | Mo = Months | Y = Years \n\n For a SINGLE Date/Time, set Step Size to reach beyond the Stop Date/Time \n so that only the Start Date/Time will be used.'
style='background:#EFEFEF; text-align:center; line-height:180%;' colspan='3'>Step Size<br>
<input name="StepSize"  type="text" value="
$StepSize"  size="16" maxlength="16">
</td>


</tr>
</table>


<!-- LONGITUDE/LATITUDE/ALTITUDE SETTINGS TABLE --->
<table width="
$TableWidth" align="bottom" 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 may not be accurate. \n\n NOTE:  These coordinates are ignored for geocentric ephemerides. ">

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

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

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

</tr>
</table>




<!-- SPECIAL OPTIONS TABLE 1 --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3"><tr>
<!-- Reference System ICRF or B1950/FK4 --->
<td width='25%' style='background:#EFEFEF; line-height:175%;' title=" I  = Use ICRF (International Celestial Reference Frame). \n B = Use B1950/FK4 (Astrometric Ephemerides Only). ">Reference System<br>
<input name="RefSystem" type="text" value="
$RefSystem"  size="6" maxlength="5" style='color:white; background:black;'>
</td>

<!-- Geocentric Coords Setting--->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No = Use observer GPS surface coordinates (Default). \n\n Yes = Use geocentric coordinates. \n Observer GPS Lon/Lat coordinates and refraction \n settings are ignored. '>Use&nbsp;Geocentric&nbsp;Coords<br>
<input name="GeocentYN" type="text" value="
$GeocentYN" size="4" maxlength="3" style='background:black; color:white;'>
</td>

<!-- Angle Output DEG|HMS Setting --->
<td width='25%' style='background:#EFEFEF; line-height:175%;' title=' DEG = Output Hour Angles and Declinations in decimal degrees. \n\n HMS = Output Hour Angles and Declinations in HMS/DMS format. '>Angle&nbsp;Output&nbsp;DEG&nbsp;or&nbsp;HMS<br>
<input name="DEGorHMS" type="text" value="
$DEGorHMS" size="4" maxlength="3">
</td>

<!-- Refraction Setting --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No = Airless = Do NOT Apply Refraction \n Yes = Apply Standard Earth Refraction. \n\n NOTE:  Refraction settting is ignored when \n using geocentric or astrometric coordinates.'>Apply Refraction<br>
<input name='RefractYN' type='text' value="
$RefractYN" size='4' maxlength='3'>
</td>
</tr></table>




<!-- SPECIAL OPTIONS TABLE 2 --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3"><tr>

<!-- Object Data Setting --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' This will include the target body physical \n data in the returned ephemeris. '>Include Object Data<br>
<input name="ObjDataYN" type="text" value="
$ObjDataYN" size="4" maxlength="3">
</td>

<!-- Julian Date Setting --->
<td style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No = Do NOT Include the JD in the Ephemeris \n Yes = Include the JD in the Ephemeris. '>Include&nbsp;Julian Date&nbsp;(JD)<br>
<input name='JDateYN' type='text' value="
$JDateYN" size='4' maxlength='3'>
</td>

<!-- Ephemeris Header Setting --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No  =  Do NOT include the ephemeris header text in output. \n Yes  =  Include the ephemeris header text in output. \n\n The header contains text showing the ephemeris parameters
 used for the computations.'>Include&nbsp;Ephem&nbsp;Header<br>
<input name="EphemHeaderYN" type="text" value="
$EphemHeaderYN" size="4" maxlength="3">
</td>

<!-- Ephemeris Footer Setting --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" colspan='3' title=' No = Do NOT include footer text. \n Yes = Include the footer text. \n The ephemeris footer contains text explaining \n\n the meanings of the column values. '>Include Ephem Footer<br>
<input name="EphemFooterYN" type="text" value="
$EphemFooterYN" size="4" maxlength="3">
</td>
</tr></table>




<!-- SPECIAL OPTIONS TABLE 3 --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3"><tr>


<!-- Not yet defined  --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" title=' Not yet defined. '>&nbsp;<br>
<!-- <input name="ZZZ" type="text" value="ZZZ" size="4" maxlength="3"> ---> &nbsp;
</td>

<!-- Distance (Range) Units Setting --->
<td width='25%' style='background:#EFEFEF; line-height:175%;' title=' Distance units used. (NASA calls distance "range". \n\n AU = Astronomical Units = Default \n KM = Kilometers \n\n 1.0 AU = 149,597,870.7 km'>Range&nbsp;(Distance)&nbsp;Units<br>
<input name="AUorKM" type="text" value="
$AUorKM"  size="3" maxlength="2">
</td>

<!-- Suppress Range Rate Setting --->
<td width='25%' style='background:#EFEFEF; line-height:175%;' title=' Yes = Display the range rate with the distance. \n No = Do NOT display the range rate with the distance. \n\n The Range Rate (deldot) refers to the radial velocity of the \n target center towards (&minus;) or away (&plus;) from the observer. '>Suppress&nbsp;Range&nbsp;Rate<br>
<input name="SuppRangeRateYN" type="text" value="
$SuppRangeRateYN" size="4" maxlength="3">
</td>



<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" title=" Enter the word 'DEMO' to set JPL demo values. \n\n Enter the word 'RESET' to reset all program defaults. \n\n Enter the word 'NOW' to set to current Date/Time. \n\n Enter the word 'HOME' to set custom home defaults. \n\n NOT case-sensitive. ">DEMO&nbsp;|&nbsp;RESET&nbsp;|&nbsp;NOW.<br>
<input name="WXYZ" type="text" value="" size="6" maxlength="5" style='color:black; background:yellow;'>
</td>
</tr></table>





<!-- EXTRA SPECIAL OPTIONS TABLE --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3"><tr>

<!-- Lunar Phase Image Table Link  --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;">
<a href ='Lunar-Phase-Image-Table.php' target='_blank'>Moon&nbsp;Phase&nbsp;Image&nbsp;Table</a>
</td>

<!-- Moon Phase Angle  --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;">
<A href ='Lunar-Phase-Angle-Table.php' target='_blank'>Moon&nbsp;Phase&nbsp;Angle&nbsp;Table</A>
</td>


<!-- Rise/Transit/Set Times  --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;">
<a href ='Rise-Transit-Set-Times.php' target='_blank'>Rise/Transit/Set Times</a>
</td>


<!-- Not yet defined  --->
<td width='25%' style="background:#EFEFEF; text-align:center; line-height:175%;" title=' Not Yet Defined. '>
<a href ='DDD.xxx'>DDD</a>
</td>
</tr></table>




<!-- REQUESTED QUANTITIES TABLE --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr><td
title=' These are the numerical codes for the requested quantities. Following the
 date, table columns will be in the same sequence as the given quantity
 codes.

 Some quantities return two ephemeris columns (Such as RA, Decl). '
style='line-height:200%; background:LightYellow; border-radius:0px 0px 8px 8px;'>
Requested Quantities ?<br>
<input name="Quantities" type="text" value="
$Quantities"  size="81" maxlength="80"><br>
<a href="Quantities-List.php" target='_blank'>&nbsp;View Quantity Codes Listing&nbsp;</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a title=' This opens a page in another tab to display \n some general program usage tips. ' href="Ephemeris-Info.php" target='_blank'>&nbsp;View Ephemeris Info&nbsp;</a>
</td></tr>
</table>






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



<!-- Source Code View and/or Download Link Table. --->
<table width="
$TableWidth">
<tr>
<td colspan="99" style="font-size:10pt; color:GreenYellow; background:black; text-align:center;">
<b><a href='View-Source-Code.php' target='_blank'>View/Copy PHP Source Code</a></b><br>
<a href='General-Ephemeris-Tool.7z'>Download PHP Source Code + Images (Approx.  3.9 MB)</a>
</td>
</tr>
</table>



<!-- Define TextArea1 --->
<table width="
$TableWidth" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:left; color:yellow; background-color:black;"><br><b> Double-Click Within Text Area to Select ALL Text </b><br>
<textarea ID="TextArea1" name="TextArea1" style="color:black; background:white; 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="bottom" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:left; color:yellow; background:black;"><br>
<b> Double-Click Within Text Area to Select ALL Text </b><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>
</td>
</tr>
</table>
--->



<!-- Page footer --->
<table width="666" align="bottom" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="color:GreenYellow; background:black; text-align:center;">PHP Program 
$_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_WEB_PAGE;



/*
   ###########################################################################
   This function returns a simple 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 General_Ephem ($TargObjID,$TimeScale,$TimeZone,$StartDateTime,
                           
$StopDateTime,$StepSize,$LonDeg,$LatDeg,$AltMet,
                           
$DaySumYN,$RefractYN,$DEGorHMS,$ObjDataYN,
                           
$SuppRangeRateYN,$JDateYN,$AUorKM,$RefSystem,
                           
$GeocentYN,$Quantities)
{

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

   
$TargObjID trim($TargObjID);
   
$Command   URLEncode($TargObjID);
   
$AltKm     trim($AltMet) / 1000;

// --------------------------------------------------------------------------
// Adjust for TT scale.  Time Zone and Daylight/Summer Time mode are ignored.

   
$TimeScale = (StrToUpper(substr(trim($TimeScale),0,1)) == 'T')? 'TT':'UT';
                 if (
$TimeScale == 'TT')
                    {
$TimeZone '+00:00'$DaySumYN 'No';}

/* -----------------------------------------------------------
   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);
   
$DSSTAdj = ($DaySumYN == 'N')? 0:1;
   list(
$TZHH$TZmm) = PReg_Split("[\:]"$TimeZone);
   
$TZSign substr($TZHH,0,1);
   
$TZHours = (($TZSign == '-')? -1:1)*(abs($TZHH) + $TZmm/60) + $DSSTAdj;
   
$i StrPos($TZHours'.');  if ($i == FALSE) {$TZHours .= '.00';}
   
$i StrPos($TZHours'.');
   
$TZHH $TZSign.SPrintF("%02d"abs(substr($TZHours,0,$i)));
   
$TimeZone "$TZHH:$TZmm";

   
$AUorKM = (StrToUpper(substr($AUorKM,0,1)) == 'K')? 'KM':'AU';

   
$RefSystem = ((substr(StrToUpper(trim($RefSystem)),0,1)) == 'B')? 'B1950':'ICRF';

   
$ObjDataYN = ((substr(StrToUpper(trim($ObjDataYN)),0,1)) == 'Y')? 'YES':'NO';

   
$Center = ((substr(StrToUpper(trim($GeocentYN)),0,1)) == 'Y')? '500':'COORD';

   
$SuppRangeRateYN = ((substr(StrToUpper(trim($SuppRangeRateYN)),0,1)) == 'Y')? 'YES':'NO';

   
$RefractYN substr(StrToUpper(trim($RefractYN)),0,1);
   
$REFRACTEDorAIRLESS = ($RefractYN == 'Y')? 'REFRACTED':'AIRLESS';

   
$DEGorHMS = (StrToUpper(substr(trim($DEGorHMS),0,1)) == 'D')? 'DEG':'HMS';

   
$CalFormat = ((substr(StrToUpper(trim($JDateYN)),0,1)) == 'Y')? 'BOTH':'CAL';

   
$Quantities trim($Quantities);

   
$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='$CalFormat'"                .
   
"&REF_SYSTEM='$RefSystem'"                .
   
"&RANGE_UNITS='$AUorKM'"                  .
   
"&SUPPRESS_RANGE_RATE='$SuppRangeRateYN'" .
   
"&ANG_FORMAT='$DEGorHMS'"                 .
   
"&APPARENT='$REFRACTEDorAIRLESS'"         .
   
"&CENTER='$Center@399'"                   .
   
"&COORD_TYPE='GEODETIC'"                  .
   
"&SITE_COORD='$LonDeg,$LatDeg,$AltKm'"    .
   
"&TIME_DIGITS='SECONDS'"                  .
   
"&TIME_ZONE='$TimeZone'"                  .
   
"&START_TIME='$StartDateTime $TimeScale'" .
   
"&STOP_TIME='$StopDateTime'"              .
   
"&STEP_SIZE='$StepSize'"                  .
   
"&EXTRA_PREC='YES'"                       .
   
"&CSV_FORMAT='YES'"                       .
   
"&QUANTITIES='$Quantities'"               ;
// ===========================================



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

   return 
HTMLEntities($TopoEphem);

// End of  General_Ephem(...)








/* ###########################################################################
   The KERNAL - 2025

   THESE ARE CUSTOM TOOLS DESIGNED FOR USE WITH THE NASA/JPL HORIZONS API.

   The code can be added to a program internally or externally via the PHP
   'include()' directive.  It can be changed or expanded upon as needed.

   Author   : Jay Tanner - 2025
   Version  : 2460692
   Language : PHP v8.2.12
   License  : Public Domain

   --------------------------------------------------------------------
   KERNAL CUSTOM EPHEMERIS PARSING VARIABLES, FUNCTIONS AND ARGUMENTS.

   UNNEEDED REFERENCES CAN BE REMOVED.    IF ONLY A FEW ARE NEEDED, THEN
   THEY COULD BE COPIED INTO YOUR SCRIPT AND THEN NO INCLUDED FILES FROM
   OUTSIDE WOULD BE NEEDED.

   Get_Horizons_Tech_Data ()
   Special_Horizons_Query (Query)

   Get_Obj_Data           (TargObjID)
   Get_Target_Name        (TargObjID)
   Get_Ephem_Header       (RawEphem)
   Get_CSV_Labels         (RawEphem)
   Get_CSV_Body           (RawEphem)
   Get_Column_Meanings    (RawEphem)
   Split_Days             (RawEphem)
   HMS_to_Hours           (HMSString, Decimals)
   Hours_to_HMS           (Hours, Decimals)
   DMS_to_Deg             (DMSString, Decimals)
   Compass_Symbol         (AzimDeg)
   Lunar_Phase_to_Text    (PhaseAngDeg)
   JD_Num                 (BCADDateStr, JGAMode)
   Inv_JD_Num             (JDNumber, JGAMode)
   Is_Valid_Date_Str      (BCADDateStr)
   ###########################################################################
*/





/*
   ###########################################################################
   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 decimal degrees equivalent to a DMS string argument
   to the specified number of decimals.

   The Degrees, Minutes and Seconds string values are separated by spaces.

   Generic.  No special error checking is done.

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

   
function DMS_to_Deg ($DMSString$Decimals=14)
{
   
$DDmmss   trim($DMSString);
   
$decimals trim($Decimals);

// -----------------------------------
// Account for any numerical +/- sign.

   
$NumSign substr($DDmmss,0,1);
   if (
$NumSign == '-')
      {
$DDmmss substr($DDmmss,1,StrLen($DDmmss));}
   else
      {
       if (
$NumSign == '+')
          {
$DDmmss substr($DDmmss,1,StrLen($DDmmss));}
           
$NumSign '+';
      }

// -----------------------
// Remove all white space.

   
$DDmmss PReg_Replace("/\s+/"" "$DDmmss);

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

   
$n  Substr_Count($DDmmss' ');

   
$dd $mm $ss 0;

// --------------------------------------
// Collect all given time element values.
// They can be integer or decimal values.

   
for ($i=0;   $i 1;   $i++)
   {
   if (
$n == 1){list($dd)         = PReg_Split("[ ]"$DDmmss);}
   if (
$n == 2){list($dd,$mm)     = PReg_Split("[ ]"$DDmmss);}
   if (
$n == 3){list($dd,$mm,$ss) = PReg_Split("[ ]"$DDmmss);}
   }

// ----------------------------------
// Compute DMS equivalent in degrees.

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

// End of  DMS_to_Deg(...)





/*
   ###########################################################################
   This function extracts and returns ONLY target object name text for any
   given solar syetem object ID# or record #, if such data exists.

   Since there are many, many, many thousands of asteroids, there are many of
   them without actual names like Ceres or Vesta, but identified by a variety
   of various catalog symbols that may seem cryptic at first.

   Example: For Target Object ID = 9941;
   Returns: 9941 Iguanodon (1989 CB3)

   NOTE:
   A semicolon (;) at the end of the ID means an asteroid or small body.

   EXAMPLES:

   Given the Target Object ID = 301
   The returned target name string would be:  'Moon (301)'


   Given the Target Object  ID = 301;
   The returned target name string would be:  '301 Bavaria (A890 WA)'

   ----------------------------
   Given the asteroid BodyID #;
   2934;

   The returned target name string would be:
   '2934 Aristophanes (4006 P-L)'

   If no target object name is found, then an error message is returned.

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

   
function Get_Target_Name ($TargObjID)
{
// ===========================================================================
// Read arguments.

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

// ---------------------------
// Set to current system date.

   
$StartDate date("Y-M-d");

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='$Command'"    .
   
"&OBJ_DATA='NO'"         .
   
"&MAKE_EPHEM='YES'"      .
   
"&EPHEM_TYPE='OBSERVER'" ;
// ===========================================================================

/* ----------------------------------------------------------
   Send query to Horizons API to obtain the target body name.
*/
   
$W Str_Replace(",\n"" \n"File_Get_Contents($From_Horizons_API));

/* -----------------------------------
   Check if an ephemeris was returned. If
   not, then return an empty string ('').
*/
   
if (StrPos($W'$$SOE') === FALSE) {return trim($W);}

/* ----------------------------------------------------------
   Get the NASA/JPL Horizons target body name string, if any.
   Not every target body may yet have a fixed name or ID, but
   may possibly still have a record # and ephemeris.
*/
   
$T trim($W);
   
$i StrPos($T'Target body name:');
   
$j StrPos($T'}');
   
$W PReg_Replace("/\s+/"" "trim(substr($T$i$j-$i+1)));
   
$W trim(Str_Replace('Target body name:',''$W));
   
$i StrPos($W'{');

   return 
trim(substr($W0$i));

// End of  Get_Target_Name (...)





/*
   ###########################################################################
   This function extracts and returns ONLY the ephemeris parameters text.  If
   the given data does NOT contain an ephemeris, then FALSE is returned.

   The parameters text contains the body ID and ephemeris settings.

   The ephemeris parameters section always begins with the characters:
   'Ephemeris / API_USER'

   Generic.  No special error checking is done.

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

   
function Get_Ephem_Header($RawEphem)
{
   
$T trim($RawEphem);
   
$i StrPos($T"\$\$SOE\n");  if ($i === FALSE) {return $i;}
   
$i StrPos($T'Ephemeris / API_USER');
   
$T substr($T$iStrLen($T));
   
$j StrPos($T'Date_');
   
$T substr($T0$j);
   
$i StrPos($T'(spreadsheet)') + 13;
   
$T substr($T0$i);
   return 
trim($T) . "\n" Str_Repeat("="80) . "\n";
}






/*
   ###########################################################################
   This function simply returns the basic physical data for any given body ID
   or space mission cataloged by NASA/JPL and also execute special Horizons
   queries.

   The body ID can be a unique name, NASA Body ID # or an SPK ID# DES=xxxxxxx
   or a special info query or directive.

   Generic.  No special error checking is done.

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

   
function Get_Obj_Data ($TargObjID)
{
// ===========================================================================
// Construct query URL for the NASA/JPL Horizons API.

   
$Command URLEncode(trim($TargObjID));

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='$Command'" .
   
"&MAKE_EPHEM='NO'"    .
   
"&OBJ_DATA='YES'"     ;

// ===========================================================================
// Send query to Horizons API to obtain the basic physical data for the given
// body ID as a single long text string with formatting control codes.

   
$w File_Get_Contents($From_Horizons_API);

   return 
$w;

// End of  Get_Obj_Data(...)







/*
   ###########################################################################
   This function extracts and returns ONLY the ephemeris CSV labels line.
   If the given data does NOT contain an ephemeris, then FALSE is returned.

   The column header line always begins with the characters: 'Date_'

   Generic.  No special error checking is done.

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

   
function Get_CSV_Labels ($RawEphemText)
{
   
$T trim($RawEphemText);
   
$i StrPos($T"\$\$SOE\n");  if ($i === FALSE) {return $i;}
   
$T substr($T,0,$i);
   
$T substr($TStrPos($T'Date_'), StrLen($T));
   
$i StrPos($T'*');
   
$T RTrim(substr($T0$i));
   return 
$T ";
}





/*
   ###########################################################################
   This function can be used to extract any ephemeris table body from all of
   the other extraneous text that surrounds it. An ephemeris table can consist
   of 1 line up to thousands of lines of CSV data.

   The ephemeris CSV data lines are located between two markers:

   $$SOE = Start Of Ephemeris marker
           and
   $$EOE = End Of Ephemeris marker

   Generic.  No special error checking is done.

   NO DEPENDENCIES
   ###########################################################################
*/
   
function Get_CSV_Body ($RawEphemText)
{
// ---------------------------------
// Read raw ephemeris text argument.

   
$w trim($RawEphemText);

/* -------------------------------------------------
   Set pointers to start and end of ephemeris table.
   If there is no ephemeris found, then return the
   raw text as-is, nothing is done. It may possibly
   be an error or some other text instead.
*/
   
$i StrPos($w"\$\$SOE\n"); if ($i === FALSE) {return $w;}
   
$j StrPos($w"\$\$EOE\n");

/* --------------------------------------------
   Extract ONLY the required ephemeris CSV data
   line(s) from between the Start/End pointers.
*/
   
$EphemTableText trim(substr($w$i+5$j-$i-5));

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

// End of  Get_CSV_Body (...)





/*
   ###########################################################################
   This function extracts and returns ONLY the ephemeris footer text.  If the
   given data does NOT contain an ephemeris, then FALSE is returned.

   The footer text explains the meanings of the ephemeris data columns.

   The ephemeris footer text always begins with the characters:
   'Column meaning:'

   Generic.  No special error checking is done.

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

   
function Get_Column_Meanings ($RawEphemText)
{
   
$T trim($RawEphemText);
   
$i StrPos($T'$$SOE');  if ($i === FALSE) {return $i;}
   
$i StrPos($T'Column meaning:');
   
$T substr($T$iStrLen($T));
   
$w trim($T);
   return 
"\n" Str_Repeat('*'80)."\n".substr($w0StrPos($T'*******'))
        . 
Str_Repeat('*'80);

// End of  Get_Column_Meanings (...)


/*
  ############################################################################
  This functions splits an ephemeris at at points where the day changes.

  Generic.  No special error checking is done.

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


   
function Split_Days ($MarkedListText)
{
   
$T trim($MarkedListText);

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

// -------------------------------------------------------------------
// Extract ONLY the CSV ephemeris table from a marked raw dated table.

// -------------------------------------------------
// Set pointers to start and end of ephemeris table.

   
$i StrPos($T'$$SOE');
   
$j StrPos($T'$$EOE');

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

// -------------------------------
// Store CSV text block in working
// array and count the elements.

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

// --------------------------------------------------------------
// Construct table with different dates separated by blank lines.

   
$FirstLine trim($wArray[0]);

   if (
Is_Numeric(substr($FirstLine,0,1))) {$FirstLine $FirstLine";}

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

   if (
Is_Numeric(substr($PrevLine,0,1))) {$PrevLine $PrevLine";}
   if (
Is_Numeric(substr($CurrLine,0,1))) {$CurrLine $CurrLine";}

   
$PrevDay substr($PrevLine,10,2);
   
$CurrDay substr($CurrLine,10,2);

/* --------------------------------------------------
   Compare previous and current day numbers to see if
   they are equal.   If not, then insert a blank line
   before printing the subsequent ephemeris line.
*/
   
if ($PrevDay == $CurrDay)
      {
$wTable .= "$CurrLine\n";}
   else
      {
$wTable .= "\n$CurrLine\n";}
  }
   return 
RTrim("$FirstLine\n$wTable");

// End of  Split_Days(...)





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

   The compass is divided into 16 named
   cardinal zones.

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

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

   Generic.  No special error checking is done.

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

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

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

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

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

   
$w substr("$w   ",0,3);
        if (
substr($w, -2) == '  ') {$w ' '.trim($w).' ';}

   return 
$w;

// End of  Compass_Symbol (...)




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

   ------------------------------------------------------
   The simple lunar phase angle = 0 to 360 degrees
   reckoned clockwise where:

     0 = New Moon
    90 = First Quarter Moon
   180 = Full Moon
   270 = Last Quarter Moon
   360 = New Moon

   ERRORS
   Returns FALSE on error if phase angle
   is non-numeric or out of valid range.

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

   
function Lunar_Phase_to_Text ($PhaseAngDeg)
{
// Round absolute phase angle to nearest integer value.
   
$a trim($PhaseAngDeg);
        if (!
Is_Numeric($a) or FloatVal(trim($a)) > 360)  {return FALSE;}
   
$a abs(round(FloatVal($a),0));

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

// End of  Lunar_Phase_to_Text (...)




/*
   ###########################################################################
   This function simply returns the technical details on using the API and is
   invoked when the ?! query is entered.

  Generic.  No special error checking is done.

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

   
function Get_Horizons_Tech_Data ()
{
// ===========================================================================
// Construct query URL for the NASA/JPL Horizons API.

   
$Command URLEncode("?!");

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='$Command'" .
   
"&MAKE_EPHEM='NO'"    .
   
"&OBJ_DATA='YES'"     ;

// ===========================================================================
// Send query to Horizons to obtain the technical details on using the API.

   
$w File_Get_Contents($From_Horizons_API);

   return 
HTMLEntities($w);

// End of  Get_Horizons_Tech_Data()





   
function Special_Horizons_Query ($Query)
{
   
$Command URLEncode(trim($Query));

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='$Command'" .
   
"&MAKE_EPHEM='YES'"   .
   
"&OBJ_DATA='NO'"      ;

// ===========================================================================
// Send query to Horizons API to obtain the basic physical data for the given
// body ID as a single long text string with formatting control codes.

   
$W File_Get_Contents($From_Horizons_API);

   return 
HTMLEntities($W);
}



/*
   ###########################################################################
   This function returns the Julian Day Number for any given calendar date in
   the range from BC 19999 to AD 19999 on the old Julian calendar or on the
   modern Gregorian calendar system.

   It returns negative JD numbers for dates prior to mathematical origins of
   the respective calendar systems.

   --------------------
   CALENDAR YEAR RANGE:

   BC 19999  to  AD 19999
   There is no calendar year 0 (zero).


   --------------------------------------------------------
   For the mathematical year number (y) used in calendrical
   computations for a BC calendar year Y = ('BC Year'):
   Y = 'BC 1949'  --->  y = '-1948'

   We take the numerical value  following the 'BC' prefix
   and subtract 1  and then change the result to negative
   to obtain the mathematical year value required for use
   in the (JDNum) computation.  This adjustment will only
   apply to 'BC' date strings.

   Example:  'BC 1949-May-20'
              y = -(Y - 1)
                = -(1949 - 1)
                = -1948
   So, (y,m,d)  = (-1948,5,20)


   -------------------------------------------------------------
   Positive calendar year numbers do NOT require any adjustment.
   Example:  'AD 1949-May-20'
   So, (y,m,d) = (1949,5,20)


   --------------------------------------------
   MATHEMATICAL ORIGINS OF THE CALENDAR SYSTEMS

   BC 4713-Jan-01-Mon   JDNum = 0   On the old Julian calendar
   BC 4714-Nov-24-Mon   JDNum = 0   on the modern Gregorian calendar

   NOTE:
   If a year is  given as a negative number,  it refers to a 'BC' year
   and will be converted to 'BC|AD' format internally for computations
   because 'BC' years first require a special numerical adjustment not
   needed for 'AD' years.

   Month = 1 to 12 or as 3-letter abbreviation string ('Jan' to 'Dec').
   Date strings are NOT case-sensitive.

   The returned signed JD Number is left-space-padded to 8 characters
   to facilitate easy columnar alignment if used for tabulation.

   ARGUMENTS:
   $BCADDateStr = Date string in BC|AD format.
                  VALID EXAMPLES:
                  'BC 9949-May-20'
                  'AD 1949-5-20'
                  '16959-1-29'
                  '-1023-Nov-15'

   JGAMode = Calendar mode to apply, where:
             'A' = Auto-select mode = Default
             'G' = Gregorian
             'J' = Julian

   ERRORS:
   FALSE is returned if an invalid argument is detected.

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

   
function JD_Num ($BCADDateStr$JGAMode='A')
{
// --------------------------------------------------
// Read and adjust input date string argument format.

   
$BCADDateStr PReg_Replace("/\s+/"" "trim($BCADDateStr));


/* ---------------------------------------------------
   If first character is a minus sign (negative year),
   then convert it into a 'BC' calendar year string.

   '-1949' becomes ---> 'BC 1949'
*/
   
if (substr($BCADDateStr,0,1) == '-')
      {
$BCADDateStr 'BC ' substr($BCADDateStr1StrLen($BCADDateStr));}


// ---------------------------------------------------------------
// If no 'BC|AD' prefix at all, then attach a default 'AD' prefix.

   
$ww StrToUpper(substr($BCADDateStr,0,2));
   if (
$ww <> 'BC' and $ww <> 'AD') {$BCADDateStr "AD $BCADDateStr";}


// ------------------------------
// Read and parse date arguments.

   
list($BCADYear,$Month,$Day) = PReg_Split("[-]"$BCADDateStr);


// ------------------
// A few adjustments.
   
$BCADYear trim($BCADYear);
   
$Month    trim($Month);
   
$Day      trim($Day);


// -----------------------------------
// Get BC|AD prefix and calendar year.

   
$BCAD StrToUpper(substr($BCADYear0,2));
   
$Y trim(substr($BCADYear2StrLen($BCADYear)));


// ---------------------------------
// Adjust for BC year, if necessary.

   
if ($BCAD == 'BC') {$Y = -$Y;}


// ------------------------------------------------------------
// Read calendar year argument value and return FALSE on error.

   
$w abs($Y);  if ($w < -19999 or $w == or $w 19999)  {return FALSE;}


// ---------------------------------------------------
// Read month argument. Could be a string or a number.

   
$m UCFirst(substr(StrToLower(trim($Month)),0,3));


// ------------------------
// Read day argument value.

   
$d trim($Day);


/* ----------------------------
  Read calendar mode argument.
  'G' = Gregorian | 'J' = Julian
  'A' = Auto-select = Default
*/
   
$JGAMode substr(StrToUpper(trim($JGAMode)),0,1);
   if (
$JGAMode == '') {$JGAMode 'A';}


// -------------------------------------------------
// Define abbreviations for month and weekday names.

   
$MONTHS   'JanFebMarAprMayJunJulAugSepOctNovDec';
   
$WEEKDAYS 'SunMonTueWedThuFriSat';
   
$JGDiff   0;


/* -----------------------------------------------------
   If month is a 3-letter abbreviation ('Jan' to 'Dec'),
   then replace it with the month number 1 to 12, if
   possible.  Otherwise, return FALSE if it cannot
   resolve the abbreviation text.
*/
   
if (!Is_Numeric($m))
      {
       
$i StrPos($MONTHS$m);
       if (
$i === FALSE) {return $i;}
       
$m $i/3;
      }

// ---------------------------------------
// Error if invalid month number.

   
if ($m or $m 12) {return FALSE;}


/* -------------------------------------------------
   Proceed to compute the Julian calendar JD Number.
   This is the base JD Number value. If the Gregorian
   calendar is selected, then the difference between
   the calendars is applied to obtain the Gregorian
   calendar JD Number.
*/
   
$A floor((14-$m) / 12);
   
$B = (($Y 0)? $Y+$Y) - $A;
   
$C floor($B/100);

   
$JDNum floor(30.6001*(12*$A $m 1))
          + 
floor(365.25*($B 4716)) - 1524 $d;


/* ----------------------------------------------
   Handle automatic calendar mode selection.
   If calendar mode = 'A' = Auto-select, then the
   calendar mode is automatically determined by
   the computed JD Number value.
*/

   
if ($JGAMode == 'A')
      {
       
$JGAMode = ($JDNum 2299161)? 'J':'G';
      }


/* ---------------------------------------------
   Handle Gregorian (= default) calendar mode by
   by ADDING the difference  in days between the
   Julian and Gregorian JD Numbers, if indicated
   by the JGAMode setting.  This value could be
   negative or positive, depending on the given
   date.  Default Logic: If not 'J', then 'G'.

   GregorianJDNum = JulianJDNum + (+-JGDiff)
*/

   
if ($JGAMode <> 'J')
      {
       
$A = ($Y 0)? $Y+$Y;
       
$B trim($m);
       
$C $A floor((14-$B) / 12);
       
$D floor($C/100);
       
$JGDiff = (floor($D/4) - $D 2);
      }
       
$JDNum += $JGDiff;


/* -------------------------------------------------
   Error if (JDNum) is outside of the valid calendar
   range from 'BC 19999-Jan-01' to 'AD 19999-Dec-31'.
*/
   
if ($JDNum < -5583211 or $JDNum 9025909) {return FALSE;}


/* -----------------------------------------------
   Left-pad the signed JD number digits field with
   spaces to span exactly 8 characters width, just
   in case the values are used in a table.  This
   helps to arrange any (JDNum) column uniformly.

   The white space can be removed by performing
   a simple trim(JDNum) command, if not wanted.
*/
   
$JDNum SPrintF("% 8d"$JDNum);


// DONE:
   
return $JDNum;

// End of  JD_Num(...)










/*
   ###########################################################################
   This function is the inverse of the JD number function. Given any signed
   JD Number, it will return the corresponding calendar date string
   in 'BC|AD Yyyy-Mmm-dd-DoW' format.

   CALENDAR YEAR RANGE:
   BC 9999  to  AD 9999
   There is no calendar year 0 (zero).

   Mathematical origins of the calendar systems:
   BC 4713-Jan-01-Mon   JDNum = 0   On old Julian calendar
   BC 4714-Nov-24-Mon   JDNum = 0   On modern Gregorian calendar

   ARGUMENT:
   JDNumber = Julian Day number for the calendar date to be computed.

   JGAMode = Calendar mode
             'G' = Gregorian
             'J' = Julian
             'A' = Auto-select mode = Default

   RETURNS:
   Calendar date string in  'BC|AD Yyyy-Mmm-dd-DoW'  format
   according to the selected calendar mode.

   ERRORS:
   FALSE is returned if JD number argument is non-numeric.

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

   
function Inv_JD_Num ($JDNumber$JGAMode='A')
 {
   
$JDNum trim($JDNumber);

// -------------------------------------------
// Define Month and Day-Of-Week abbreviations.

   
$MONTHS   'JanFebMarAprMayJunJulAugSepOctNovDec';
   
$WEEKDAYS 'SunMonTueWedThuFriSat';

// ----------------------------
// Read calendar mode argument.
// 'G' = Gregorian = Default
// 'J' = Julian
// 'A' = Auto-select

   
$JGAMode substr(StrToUpper(trim($JGAMode)),0,1);
   if (
$JGAMode == '') {$JGAMode 'A';}

// ----------------------------------------------
// If calendar mode = 'A' = Auto-select, then the
// calendar mode is automatically determined by
// the given JD Number argument.

   
if ($JGAMode == 'A')
      {
       
$CMode  = ($JDNum 2299161)? 0:1;
       
$JGAMode = ($CMode == 0)? 'J':'G';
      }
   else
      {
       
$CMode = ($JGAMode == 'J')? 0:1;
      }

// -----------------------------------------
// Compute numerical date elements (y, m, d)
// according to the calendar mode selection.

  
$A floor($JDNum 0.5);
  
$B $CMode*floor(($A 1867216.25) / 36524.25);
  
$C $A $CMode*($B floor($B/4) + 1);
  
$D $C 1524;
  
$E floor(($D 122.1) / 365.25);
  
$F floor(365.25 $E);
  
$G floor(($D $F) / 30.6001);
  
$d $D $F floor(30.6001 $G);     // Day num   (1 to 31)
  
$m $G 12*floor($G/14) - 1;          // Month num (1 to 12)
  
$y $E 4716 floor((14 $m) / 12); // Mathematical year
  
$Y = ($y 0)? $y $y-1;               // Calendar year (Negative = BC)

// ------------------------------------------------------------
// At this point we have the numerical date elements (Y, m, d).
// The next step is to construct the full calendar date text
// string for output. EXAMPLE OUTPUT: 'BC 9998-May-20-Tue'

   
$i     = (+ ($JDNum 1) % 7) % 7;
   
$DoW   substr($WEEKDAYS3*$i3);
   
$Y     Str_Replace('-''BC 'SPrintF("%+05d"$Y));
   
$Y     Str_Replace('+''AD '$Y);
   
$Mmm   substr($MONTHS3*($m-1), 3);
   
$dd    SPrintf("%02d"$d);
   
$JDNum SPrintF("% +8d"$JDNum);

// DONE.
   
return "$Y-$Mmm-$dd-$DoW";

 } 
// End of  Inv_JD_Num(...)




/*
   ###########################################################################
   This function checks for an  invalid date string and returns boolean FALSE
   for an invalid date. If the date is OK, then it is returned in the same
   formalized format as 'BC 9949-Nov-01', 'AD 0149-May-20' or 'AD 0009-Jun-09'

   In formalized format, the year will always be expressed as 4 digits, padded
   by zeros as needed, like those in the above examples.

   NO DEPENDENCIES
   ###########################################################################
*/
   
function Is_Valid_Date_Str ($BCADDateStr)
{
   
$BCADDate StrToUpper(trim($BCADDateStr));

// --------------------------------
// Define month name abbreviations.

   
$MONTHS 'JanFebMarAprMayJunJulAugSepOctNovDec';


// --------------------------------------------------------
// Account for a negative year input and change it to 'BC'.

   
$BCAD '';

   if (
substr($BCADDate,0,1) == '-')
      {
       
$BCADDate 'BC '.substr($BCADDate,1,StrLen($BCADDate));
       list(
$BCAD$YearMd) = PReg_Split("[ ]"$BCADDate);
       list(
$Year,$Month,$Day) = Preg_Split("[-]"$YearMd);
       
$BCADDate 'BC ' SPrintF("%04d"$Year)."-$Month-$Day";
       
$BCAD 'BC';
      }

// ----------------------------------------------
// Error if missing any (-) separators.

   
if (Substr_Count($BCADDate'-') <> 2) {return FALSE;}



// -------------------------------------------------
// Break up date elements. Error if missing element.

   
$wArray PReg_Split("[-]"$BCADDate);
   
$wCount count($wArray);  if ($wCount <> 3){return FALSE;}



// -------------------------------------------
// Make year into a formal BCAD year string in
// the same format as 'AD 1776-Jul-04'

   
$Year StrToUpper(trim($wArray[0]));
   
$Y '';

   for(
$i=0;   $i StrLen($Year);  $i++)
  {
   
$CurrChar substr($Year,$i,1);
   if (
Is_Numeric($CurrChar)) {$Y .= $CurrChar;}
  }
   
$FirstChar substr($Year,0,1);

   if (
$FirstChar == 'B') {$BCAD 'BC';}
   if (
$FirstChar == 'A') {$BCAD 'AD';}
   if (
Is_Numeric($Year))
      {
       
$BCAD = ($Year 0)? 'BC' 'AD';
      }
   if (
$BCAD == '') {return FALSE;} // Error if not BC or AD.

   
$Year "$BCAD ".SPrintF("%04d"$Y);


// -------------------------------------------------------
// Make numeric month into a 3-letter abbreviation string.

   
$Month UCFirst(StrToLower(substr(trim($wArray[1]),0,3)));

   if (!
Is_Numeric($Month))
      {
       
$i StrPos($MONTHS$Month);  if ($i === FALSE) {return $i;}
       
$Month $i/3;
       
$m $Month;
      }

   if (
Is_Numeric($Month))
      {
       if (
$Month or $Month 12) {return FALSE;}
       
$m $Month;
       
$Month substr($MONTHS3*($Month-1), 3);
      }

   
$Day trim($wArray[2]);  if (!Is_Numeric($Day)) {return FALSE;}
     
$d $Day;  if ($d or $d 31) {return FALSE;}
   
$Day SPrintF("%02d"$Day);



// --------------------------------------------------------------
// Read year argument. (BC converts to negative year internally).

   
if (StrPos($Year'BC') !== FALSE)
      {
$Y = -$Y;}



/* -------------------------------------------------------
   If negative year, then convert it to mathematical year.
   If positive, then no adjustment is needed.
*/
   
$Y += ($Y 1)? 1:0;



/* ---------------------------------------------------------
   Automatically determine leap year adjustment according to
   the 'J|G' = 'Julian|Gregorian' calendar mode switch.
   'J' = Julian calendar for all years up to AD 1582.
   'G' = Gregorian calendar for all years from AD 1583 onward.
*/

   
$JG = ($Y 1583)? 'J':'G';

   
$LYFlag = ($JG == 'J')? $Y == :
             (
$Y == and $Y 100 <> 0) or $Y 400 == 0;
   
$LYAdj  = ($LYFlag)? 1:0;


// --------------------------
// Check for leap year error.

   
if ($Month == 'Feb' and $Day == 29 and $LYFlag === FALSE) {return FALSE;}

   
$FebDays = ($Month == 'Feb' and $Day == 29 and $LYFlag)? '29':'28';


// -----------------------------
// Check for day of month error.

   
$mDays substr("31$FebDays"."31303130313130313031"2*($m-1), 2);
   if (
$d $mDays) {return FALSE;}


// -------------------------------------------------------
// Return reconstructed formal date string if all went OK.
// Otherwise, FALSE will be returned to indicate an error.

   
return "$Year-$Month-$Day";

// End of  Is_Valid_Date_Str (...)






/*
   ###########################################################################
   Given a BC|AD calendar date string, this function returns the corresponding
   mathematical date elements ('y,m,d') in a CSV format string.

   y = Mathematical year (0 = BC 0001)

   ARGUMENT:
   BCADDateStr = BC|AD Date String


   RETURNS:
   The numeric date elements ('Y,m,d') are returned in a CSV string.

   ERRORS:
   FALSE is returned if the given year is non-numeric.

   However, this function does NOT check the validity
   of the numbers it returns. It merely breaks the date
   string down into numbers, if in valid form and range.
*/

   
function BCAD_Date_Str_to_Ymd ($BCADDateStr)
{
// Read and adjust input date string argument format.
   
$BCADDateStr PReg_Replace("/\s+/"" "trim($BCADDateStr));

// Cut off any 3-letter week day abbreviation from end.
   
$uuu substr($BCADDateStr, -4);
   if (
substr($uuu,0,1) == '-')
      {
$BCADDateStr substr($BCADDateStr,0StrLen($BCADDateStr)-4);}


// If no BC|AD prefix, then attach default AD prefix.
   
$ww StrToUpper(substr($BCADDateStr,0,2));
   if (
$ww <> 'BC' and $ww <> 'AD') {$BCADDateStr "AD $BCADDateStr";}

// ------------------------------------
// Error if instant bad argument format.
   
if (Substr_Count($BCADDateStr'-') <> 2) {return FALSE;}

// ------------------------------
// Read and parse date arguments.
   
list($BCADYear,$Month,$Day) = PReg_Split("[-]"$BCADDateStr);

// ---------------------------------------------------
// A few adjustments. Month can be a number or string.
   
$BCADYear trim($BCADYear);
   
$Month    $m UCFirst(StrToLower(substr(trim($Month),0,3)));
   
$Day      $d trim($Day);

// -----------------------------------
// Get BC|AD prefix and calendar year.
   
$BCAD StrToUpper(substr($BCADYear0,2));
   
$Y trim(substr($BCADYear2StrLen($BCADYear)));

// --------------------------
// Error if Y is non-numeric.
   
if (!Is_Numeric($Y))  {return FALSE;}

// ---------------------------------
// Adjust for BC year, if necessary.
// Y = Calendar year number (Neg = BC).
   
if ($BCAD == 'BC') {$Y = -$Y;}

// ---------------------------------------------
// Error if year is 0 or outside calendar range.
// There is no year 0 (zero) on either calendar.
   
if ($Y < -19999  or  $Y 19999  or  $Y == 0)  {return FALSE;}

// -----------------------------------------------------
// If month is a 3-letter abbreviation ('Jan' to 'Dec'),
// then replace it with the month number 1 to 12, if
// possible.  Otherwise, return FALSE.

   
if (!Is_Numeric($m))
      {
       
$i StrPos('JanFebMarAprMayJunJulAugSepOctNovDec'$m);
       if (
$i === FALSE) {return $i;}
       
$m $i/3;
      }

   
$m += 0;
   
$d += 0;

// ---------------------------------------
// Error if invalid month number.
   
if ($m or $m 12) {return FALSE;}

// DONE.
   
return "$Y$m$d";

// End of  BCAD_Date_Str_to_Ymd (...)






/* ###########################################################################
   This function returns the number of days in any given month of any given
   year on the old Julian calendar or the modern Gregorian calendar.

   The month can be a number (1 to 12) or a 3-letter
   abbreviation ('Jan' to 'Dec').

   The 'AD' prefix is optional.

   -------------
   DEPENDENCIES:

   Is_Valid_Date()

   ###########################################################################
*/
   
function Days_in_Month ($BCADDateStr)
{
   
$BCADDate trim($BCADDateStr);

   
$W Is_Valid_Date_Str($BCADDate);  if ($W === FALSE) {return FALSE;}

   
$YmdStr BCAD_Date_Str_to_Ymd ($W);

/* -----------------------------------------------------------
   Get 'Y/m/d' values corresponding to given BCAD date string.
   'BC' years will be expressed as negative values.  There is
   no year 0 (zero) on either calendar.    Note the numerical
   date string element sequence is 'Y/m/d'  and  NOT 'm/d/Y".
*/

   
list($Y,$m,$d) = PReg_Split("[,]"$YmdStr);

   
$JG = ($Y 1583)? 'J':'G';

   
$LYFlag = ($JG == 'J')? $Y == :
             (
$Y == and $Y 100 <> 0) or $Y 400 == 0;
   
$LYAdj  = ($LYFlag)? 1:0;

// --------------------------
// Check for leap year error.

   
if ($m == and $d == 29 and $LYFlag === FALSE) {return FALSE;}

   
$FebDays = ($m == and $d == 29 and $LYFlag)? '29':'28';

// ------------------------------------------------
// Check if day of month is outside of valid range.

   
$mDays substr("31$FebDays"."31303130313130313031"2*($m-1), 2);
   if (
$d $mDays) {return FALSE;}

   return 
$mDays;
}





/*
   ###########################################################################
   ###########################################################################
   This function returns the simple lunar phase angle from 0 to 360 degrees
   based on the difference between the geocentric ecliptical longitudes of
   the moon and sun.  This gives a very good visual approximation to the
   geocentric lunar phase at the given local date, time and time zone.

   --------------------------------------------------------------
   Let:

   Lm = Geocentric ecliptical longitude of the moon (0 to 360 deg)
   Ls = Geocentric ecliptical longitude of the sun  (0 to 360 deg)

   PhaseAng = Simple lunar phase angle (0 to 360 deg)

   Then:

   w = 360 - Lm + Ls
   if (w > 360) then w = w - 360
   PhaseAng = 360 - w

   or equivalently:

   w = 360 - Lm + Ls
   PhaseAng = 360 - (w -= (w > 360)? 360:0)

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

   
function Lunar_Phase_Angle ($DateTimeStr$TimeZone='+00:00'$DaySumYN='N')
{
   GLOBAL 
$Cnst;
   
$DaySumYN  substr(StrToUpper(trim($DaySumYN)),0,1);
   
$DaySumAdj = ($DaySumYN == 'N')? 0:1;

/* ------------------------------------------------------
   Adjust for Daylight/Summer Time. This assumes that the
   Time Zone is given in the standard '+-HH:mm' format.
*/
   
list($TZHH$TZmm) = PReg_Split("[\:]"$TimeZone);
   
$TZSign substr($TZHH,0,1);
   
$TZSignVal = ($TZSign == '-')? -1:1;
   
$TZHours $TZSignVal * (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";

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='301'"                .
   
"&OBJ_DATA='NO'"                .
   
"&MAKE_EPHEM='YES'"             .
   
"&EPHEM_TYPE='OBSERVER'"        .
   
"&CAL_FORMAT='CAL'"             .
   
"&CAL_TYPE='MIXED'"             .
   
"&REF_SYSTEM='ICRF'"            .
   
"&APPARENT='REFRACTED'"         .
   
"&RANGE_UNITS='AU'"             .
   
"&CENTER='500@399'"             .
   
"&TIME_DIGITS='SECONDS'"        .
   
"&TIME_ZONE='$TimeZone'"        .
   
"&START_TIME='$DateTimeStr UT'" .
   
"&STOP_TIME='$DateTimeStr.1'"   .
   
"&STEP_SIZE='1d'"               .
   
"&EXTRA_PREC='YES'"             .
   
"&CSV_FORMAT='YES'"             .
   
"&QUANTITIES='31,29"            ;

// ---------------------------------------------------------------------
// Extract the geocentric ecliptical longitude and latitude of the moon.

   
$Moon Str_Replace(",\n"" \n"trim(File_Get_Contents($From_Horizons_API)));

// ----------------------------------------------------
// Set pointers to start and end of ephemeris table and
// extract ONLY the required ephemeris CSV data line
// from between the Start/End pointers.

   
$i    StrPos($Moon'$$SOE');
   
$j    StrPos($Moon'$$EOE');
   
$Moon trim(substr($Moon$i+5$j-$i-5));


   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='10'"                 .
   
"&OBJ_DATA='NO'"                .
   
"&MAKE_EPHEM='YES'"             .
   
"&EPHEM_TYPE='OBSERVER'"        .
   
"&CAL_FORMAT='CAL'"             .
   
"&CAL_TYPE='MIXED'"             .
   
"&REF_SYSTEM='ICRF'"            .
   
"&APPARENT='REFRACTED'"         .
   
"&RANGE_UNITS='AU'"             .
   
"&CENTER='500@399'"             .
   
"&TIME_DIGITS='SECONDS'"        .
   
"&TIME_ZONE='$TimeZone'"        .
   
"&START_TIME='$DateTimeStr UT'" .
   
"&STOP_TIME='$DateTimeStr.1'"   .
   
"&STEP_SIZE='1d'"               .
   
"&EXTRA_PREC='YES'"             .
   
"&CSV_FORMAT='YES'"             .
   
"&QUANTITIES='31'"              ;

// --------------------------------------------------------------------
// Extract the geocentric ecliptical longitude and latitude of the sun.

   
$Sun Str_Replace(",\n"" \n"trim(File_Get_Contents($From_Horizons_API)));



/* ----------------------------------------------------
   Set pointers to start and end of ephemeris table and
   extract ONLY the required ephemeris CSV data line(s)
   from between the Start/End pointers.
*/
   
$i   StrPos($Sun'$$SOE');
   
$j   StrPos($Sun'$$EOE');
   
$Sun trim(substr($Sun$i+5$j-$i-5));


/* --------------------------------------------------------------
   Extract ONLY the ecliptical longitude values for moon and sun.
   The latitudes are not needed for these computations.
*/
   
list($w,$w,$w$Lm,$w,$Cnst) = PReg_Split("[,]"$Moon);
   list(
$w,$w,$w$Ls) = PReg_Split("[,]"$Sun);

   
$Cnst trim($Cnst);

// --------------------------------------------------------
// Compute the simple lunar phase angle (0 to 360 degrees).

   
$w 360 $Lm $Ls;

   
$PhaseAng 360 - ($w -= ($w 360)? 360:0);

   return 
$PhaseAng;

// End of  Lunar_Phase_Angle (...)





// END OF PROGRAM


?>