<?php
/*
============================================================================
This program computes a basic DE405 lunar ephemeris for any given date in
the 600-year range from 1600 to 2200.  It displays the phase of the moon
computes the geocentric and topocentric lunar coordinates and distance for
any given moment in the given range as well as the rising and setting times
of the sun and moon.

It also computes all lunar quarter phases, apogees and perigees for the
given month as well as radial and orbital velocities and several other
useful numerical lunar statistics.

The complete package contains this PHP program script, the DE405 data
file and a set of lunar phase images at 1-degree intervals from 0 to 360
degrees, so it is a rather large file, at about 15.8 meg as a compressed
7z archive.

Author    : Jay Tanner - 2018
Language  : PHP v7.1.8

----------------------------------------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
----------------------------------------------------------------------------

*/




/* -------------------------------------
   Initialize output buffer this must be
   the very first command of the program
   when using cookies.
*/
   ob_start();

// Get time zone offset from local server.
   date_default_timezone_set('America/New_York');
   $STZStr = date_default_timezone_get();

// ----------------------------------
// Program version and revision date.

   $_ThisScriptName_ = BaseName($_SERVER["SCRIPT_FILENAME"]);
   $_ProgramVersion_ = "v2.94";
   $at = "&#97;&#116;";
   $_RevisionDate_ = date("Y F d  $at h:i:s A  l", FileMTime($_ThisScriptName_));


// DEFINE 78-CHARACTER HORIZONTAL BARS. THESE CAN
// BE CALLED GLOBALLY AS TEXT BLOCK SEPARATORS.

   $_HR1_ = str_repeat("&#183;", 78);
   $_HR2_ = str_repeat('-', 78);
   $_HR3_ = str_repeat('=', 78);
   $_HR4_ = str_repeat('*', 78);
   $_HR5_ = str_repeat("&#151;", 78);

   $_HR_ = array();

   for($i=1;   $i < 80;   $i++)
  {
   $chars = str_repeat("&#151;", $i);
   $_HR_[$i] = $chars;
  }


// Define text size for TextArea display.
   $TextSize = '18px';









// CONSTANTS
//
// ------------------------------------
// Speed of light in meters per second.
// Taken from NASA/JPL DE430 constants.

// ---------------------------------------------
// Due to the way the length of a second and the
// length of the meter are EXACTLY defined, this
// speed of light is also an EXACT definition.

   define ("CLIGHT", "299792458");

// -----------------------------------------------
// Standard Earth gravity (1 G) acceleration (SI).

   define ("G_STD", "9.80665");

// --------------------------------
// Universal gravity constant (SI).

   define ("G_UNI", "0.000000000066742");

// -------------------------------------------------------------------
// Define some EXACT standard metric length/distance interconversion
// factors in terms of meters per unit (M_PER_XXX).  This is the
// factor by which to multiply the units to obtain equivalent meters.

   define ("M_PER_A"   , "0.0000000001");  // m per angstrom
   define ("M_PER_NM"  , "0.000000001");   // m per nanometer
   define ("M_PER_UM"  , "0.000001");      // m per micrometer (micron)
   define ("M_PER_MM"  , "0.001");         // m per millimeter
   define ("M_PER_CM"  , "0.01");          // m per centimeter
   define ("M_PER_IN"  , "0.0254");        // m per inch
   define ("M_PER_FT"  , "0.3048");        // m per foot
   define ("M_PER_YD"  , "0.9144");        // m per yard
   define ("M_PER_M"   , "1");             // m per meter
   define ("M_PER_KM"  , "1000");          // m per kilometer
   define ("M_PER_MI"  , "1609.344");      // m per mile (statute)
   define ("M_PER_NMI" , "1852");          // m per nautical mile
   define ("M_PER_LS", "299792458");       // m per Light Second

   define ("KM_PER_NMI", "1.852");         // km per nautical mile
   define ("KM_PER_MI" , "1.609344");      // kilometers per statute mile

// -----------------------------
// Define the Astronomical Unit.

   define ("M_PER_AU" ,  "149597870700"); // NASA/IAU/DE430 as of 2012
   define ("KM_PER_AU",  "149597870.700");
   define ("MI_PER_AU",  "92955807.273");

// -----------------------------------------------------
// Derived factors based on the above adopted standards.
// The AU used is the IAU value and the year value used
// is the standard Julian year of exactly 365.25 days.

   define ("M_PER_LY"  , "9460730472580800"); // EXACT
   define ("M_PER_PC"  , "30856775813057289.532915512381557");
   define ("AU_PER_LY" , "63241.0770880709446742");
   define ("AU_PER_PC" , "206264.8062470963551565");
   define ("LY_PER_PC" , "3.2615637769712137");

   define ("LT_1AU"    , "0.00577551830441213");

// ----------------------------------
// DEFINE SOME WEIGHT/MASS STANDARDS.

   define ("KG_PER_OZ" , "0.028349523125"); // EXACT
   define ("KG_PER_LB" , "0.45959237");     // EXACT
   define ("G_PER_OZ"  , "28.349523125");   // EXACT

   define ("N_PER_LB"  , "4.4482216152605");

// ------------------------------------------
// LENGTH OF JULIAN CALENDAR YEAR IN SECONDS.
// EXACTLY 365.25 DAYS.

   define ("S_PER_JYR", "31557600");

// ----------------------------------
// DEFINE NUMBER OF SECONDS IN 1 DAY.

   define ("S_PER_DAY" , "86400");

// ---------------------------------------------
// LENGTH OF GREGORIAN CALENDAR YEAR IN SECONDS.
// EXACTLY 365.2425 DAYS.

   define ("S_PER_GYR", "31556952");

// ---------------------------
// Define Planck constant (h).
// 6.62606896E-34

   define ('PLANCK_h', '0.000000000000000000000000000000000662606896'); //  J/s

// -----------------------------------------
// Define Stefan-Boltzmann constant (sigma).
//
// Radiated energy = sigma * K**4 Watts per sq. meter per second
// 5.670373E-8
// Where K = temperature in kelvins

   define ('STEF_BOLT_sigma', '0.00000005670373'); // Watts/sq. meter/k^4

// ------------------------------
// Define Boltzmann constant (k).
//
// k = Joules / deg Kelvin

   define ('BOLTZ_k', '0.000000000000000000000013806504'); //  J/s/K = Watts/K


// ===================================================
// Define ALL available solar system body names. These
// values will be GLOBAL to all function modules.  All
// the fields are padded to a width of 10 characters
// with trailing white space, for uniformity.

   define ('BODY_NAMES', 'Sun       Moon      Mercury   Venus     Mars      Jupiter   Saturn    Uranus    Neptune   Pluto');
   define ('ASTEROID_NAMES', 'Ceres     Cybele    Davida    Eunomia   Europa    Flora     Hebe      Hygiea    InteramniaIris      Juno      Metis     Pallas    Psyche    Vesta');
   define ('MONTH_NAMES', 'January   February  March     April     May       June      July      August    September October   November  December');
   define ('DOW_NAMES', 'Sunday    Monday    Tuesday   Wednesday Thursday  Friday    Saturday');



// FUNCTIONS
// -------------------------------------------------------------------------
// Strips all leading, trailing and internal white space from within a text
// string.  All occurrences of multiple spaces within the text string will
// be replaced by single spaces and all leading and trailing white space
// and NewLine end-codes will also be removed.
//
// This helps facilitate reading sequential space-delimited items from
// a given text-data string by normalizing all spacing between words
// to a single space, for easier use with the preg_split() function.

   function Strip_WSpace ($TextString)
{
   return preg_replace("/\s+/", " ", trim($TextString));
}




/*
   -------------------------------------------------------------------------
   This function simply swaps in place, the
   contents of two simple variable arguments.
*/

   function Swap (&$var1, &$var2)
 {
   $w=$var1;  $var1=$var2;  $var2=$w;
 }




/*
   Sort an alpha or numeric table given as a text string argument
   delimited by NewLine codes.

   Numerical sort is in ascending order from the left and assumes that
   all strings begin with a number and end with a NewLine (\n) code.

   Otherwise, it performs a simple alphabetic text line sort.
*/

   function Sort_Table ($TableTextString)
{
   $wArray = preg_split("[\n]", RTrim($TableTextString, "\n"));
   $wCount = count($wArray);

// Sort table numerically from left.
   sort($wArray);

// Reconstruct and return table as text string.
   $w = '';
   for ($i=0;   $i < $wCount;   $i++)
   {
   $CurrLine = RTrim($wArray[$i], "\n");
   $w .= "$CurrLine\n";
   }
   return RTrim($w, "\n");

} // End of  Sort_Table(...)





/*
   ------------------------------------------------------------------
   Pads a text string with any padding character (default = space) on
   the left or right end to make XPadCount chracters in total length.
*/
   function LR_X_Pad ($TextString, $LorRStr='L', $XPadChar=' ', $XPadCount=0)
{
   $wLorRStr   = (strtoupper(trim($LorRStr)) == 'L')? 'L':'R';
   $wTextStr   = substr($TextString, 0, $XPadCount);
   $wXPadChar  = substr($XPadChar, 0,1);
   $wXPadCount = intval($XPadCount);

   if ($wLorRStr == 'L')
  {
   $w = str_repeat($wXPadChar, $wXPadCount) . $wTextStr;
   $w = substr($w, -$wXPadCount);
  }
   else
  {
   $w = substr($wTextStr, 0, $wXPadCount) . str_repeat($wXPadChar, $wXPadCount);
   $w = substr($w, 0, $wXPadCount);
  }

   return $w;
}







/*
   This function rotates a plain text string by ±n characters.

   -N = Rotate N characters left
   +N = Rotate N characters right
*/


   function str_rotate($TextString, $RotNumChars=1)

   {
   $TextStr = str_replace("\n", '', $TextString);

   $L = strlen($TextStr);
   $N = intval($RotNumChars);  if ($N == 0){return $TextString;}
   $n = abs($N) % $L;

   if ($N < 0)
  {

// Rotate left by (-n) characters.
   $LeftStr  = substr($TextStr, 0, $n);
   $RightStr = substr($TextStr, $n, $L);
   return $RightStr.$LeftStr;
  }

// Rotate right by (+n) characters.
   $LeftStr  = substr($TextStr, -$n);
   $RightStr = substr($TextStr, 0, $L - $n);
   return $LeftStr.$RightStr;

} // End of  str_rotate(...)



/*
   These two functions padd a string on the left or right ends
   with any chosen padding character (default = space).


   TextString = String to be padded
   PadLen     = Padding length

   PadChar    = Padding character to use (default = space).
                For numbers, a zero can be used instead of a space.

*/
   function L_Pad_Text ($TextString, $PadLen=0, $PadChar=' ')
{
   return substr(str_repeat(substr($PadChar, 0,1), $PadLen).$TextString, -$PadLen);
}


   function R_Pad_Text ($TextString, $PadLen=0, $PadChar=' ')
{
   return $TextString.substr(str_repeat(substr($PadChar, 0,1), $PadLen), 0,$PadLen);
}





/*
   ========================================================================
   Generates a string of any number of random digits in any given base
   from 2 to 36.

   All letters (A to Z) used as digits will be returned in uppercase.

   ERRORS:
   On error, FALSE is returned for invalid argument(s).
   ========================================================================
*/

   function Rand_Digits_Base_B ($NumDigits=1, $BaseB=10, $LZeroSuppressed=FALSE)
{

// Read arguments.
   $N = intval($NumDigits);
   $B = intval($BaseB);

// Check for invalid argument(s).
   if ($B < 2 or $B > 36 or $N < 1) {return FALSE;}

// Define digits spectrum for the given base.
   $DigitsSpectrum = substr("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",0,$B);

// Construct an N-digit random sequence in base B, one digit at
// a time, using a random 50/50 digit alternation method. There
// will be at least one random base B digit returned.
   $RandomDigits = "";

   for ($i=0;   $i < $N;   $i++)
  {
   if (mt_rand() % 2 == 0)
  {$RandomDigits .= substr($DigitsSpectrum, mt_rand(0,$B-1),1);}
   else
  {$RandomDigits = substr($DigitsSpectrum, mt_rand(0,$B-1),1) . $RandomDigits;}
  }

// If LZeroSuppressed flag === TRUE, then leftmost digit of returned
// random digits string must NOT be zero.  If it is, then another
// random non-zero digit will be attached to the left of it and
// the rightmost digit dropped.  The result will be a normal
// integer value with the required number of digits, but
// never starting with 0 (zero) as the first digit.
   if ($LZeroSuppressed === TRUE)
  {
   $RandomDigits = substr($DigitsSpectrum, mt_rand(1,$B-1),1) . $RandomDigits;
  }
   return substr("$RandomDigits", 0, $N);

} // End of  Rand_Digits_Base_B(...)




   function Rand_Int_Enc_Date()
{
   list ($m, $d, $yyyy) =
   preg_split("[\/]", JDtoGregorian(mt_rand(2305448, 2524958)));
   $mn = sprintf("%02d", $m);
   $dn = sprintf("%02d", $d);
   return "$yyyy$mn$dn";

} // End of  Rand_Int_Enc_Date()



   function Rand_Date_String()
  {
   $w = trim(JD_Z_to_DT (mt_rand(2305448, 2524958), '+00:00', 2));
   $DateStr = substr($w, 0,11);
   $DoWStr  = substr($w, -3);

   return "$DateStr  $DoWStr";

  } // End of  Ran_Date_String()








   function Rand_Time_String()
  {
   $hh = sprintf("%02d", mt_rand(0, 24));
   $mm = sprintf("%02d", mt_rand(0, 59));
   $ss = sprintf("%02d", mt_rand(0, 59));

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

   } // End of  Rand_Time_String()





   function Rand_Date_Time_String()
  {
   $RandDateStr = Rand_Date_String();
   $DoWStr      = substr($RandDateStr, -3);
   $RandDateStr = substr($RandDateStr, 0,11);
   $RandTimeStr = Rand_Time_String();

   return "$RandDateStr  $DoWStr  $RandTimeStr";

  } // End of  Rand_Date_Time_String()




   function Rand_Decimal($NumDigits=1, $RandSignFlag=FALSE)
  {
   $RandSign = '';
   $NumDigits = floor($NumDigits);
   $RandSignFlag = ($RandSignFlag === TRUE)? TRUE:FALSE;

   if ($RandSignFlag === TRUE)
      {
       $RandSign = (mt_rand() % 2 == 0)? '-':'+';
      }

   return $RandSign.'0.'.Rand_Digits_Base_B($NumDigits);

  } // End of  Rand_Decimal()









// ========================================
// Basic trigonometric functions for degree
// aguments rather than radians.

   function Sind($AngDMS)
  {
   return Sin(deg2rad(DMS_to_Deg($AngDMS)));
  }

   function Cosd($AngDMS)
  {
   return cos(deg2rad(DMS_to_Deg($AngDMS)));
  }

   function Tand($AngDMS)
  {
   return Tan(deg2rad(DMS_to_Deg($AngDMS)));
  }

   function ASind($x)
  {
   return rad2deg(asin(floatval($x)));
  }

   function ACosd($x)
  {
   return rad2deg(acos(floatval($x)));
  }

   function ATand ($x)
  {
   return rad2deg(atan(floatval($x)));
  }

   function ATand2 ($y, $x)
  {
   $w = rad2deg(atan2(floatval($y), floatval($x)));

   return $w + (($w < 0)? 360:0); // 0 to +360 degrees
  }



/*
============================================================
TEMPERATURE SCALE INTERCONVERSION FUNCTION.

This function converts from any given temperature
scale into the equivalent temperature on any one
of four other recognized (F,C,K,R) scales.

The scale symbols are:
F = Fahrenheit
C = Celsius
K = Kelvin  = Degrees Celsius above absolute zero.
R = Rankine = Degrees Fahrenheit above absolute zero.

The scale symbols are NOT case sensitive.

The conversion is performed by first converting the
input temperature value into its equivalent on the K
scale, then the K scale value is converted into the
given output scale.

-------
EXAMPLE
To convert from 123.45 C into its equivalent
on the R scale:

print Temp_To_Temp("-123.45 C", "R"); // Result = 269.46


------
ERRORS
An error returns FALSE if a scale symbol is not recognized.
============================================================
*/


   function Temp_to_Temp ($FromTempStr, $ToScaleStr)
{
// -------------------------------------
// Read input argument value and scales.

   $w = trim($FromTempStr);   if ($w == '') {$w = "0";}
   $T = floatval($w);

   $FromScale = strtoupper(trim(substr($w, -1)));
   $ToScale = strtoupper(trim($ToScaleStr));

// ---------------------------------------------------
// Convert input ($FromScale) into K scale equivalent.

   switch ($FromScale)
  {
   case "F" : $K = (5 * ($T - 32) / 9) + 273.15; break;
   case "C" : $K = $T + 273.15; break;
   case "K" : $K = $T; break;
   case "R" : $K = (5 * ($T - 491.67) / 9) + 273.15; break;
   default  : $K = FALSE; break; // On ERROR
  }

// --------------------------
// ERROR if invalid FromScale

   if ($K === FALSE) {return $K;}

// --------------------------------------------------
// Convert K value into output ($ToScale) equivalent.

   switch (strtoupper(trim($ToScale)))
  {
   case "F" : $T = (9 * ($K - 273.15) / 5) + 32; break;
   case "C" : $T = $K - 273.15; break;
   case "K" : $T = $K; break;
   case "R" : $T = (9 * ($K - 273.15) / 5) + 491.67; break;
   default  : $T = FALSE; break;
  }

   return $T;

} // End of Temp_To_Temp(...)




/*
 ===========================================================================
 This function interconverts between any of sixteen length or
 distance units.  The unit symbols are NOT case sensitive.

 ----------
 ARGUMENTS:
 $LengthAndUnits = Numerical value and units symbol of length to convert.
                   NOTE: There MUST be at least one space between the
                   number value and the units symbol in the string.
                   The unit symbols are NOT case sensitive.

 $ToUnits = The units symbol corresponding to the computed output value.

 -------
 ERRORS:
 On error FALSE is returned.
 An error occurs if the length value is non-numeric or either
 of the input or output length units symbols are unrecognized.

 ------------------------------------------------------------
 THE 16 LENGTH UNITS SYMBOLS RECOGNIZED BY THIS FUNCTION ARE:

 SYMBOL      UNITS

   A         Angstroms
   nm        Nanometers
 um or u     Micrometers (or Microns)
   mm        Millimeters
   cm        Centimeters
   in        Inches
   ft        Feet
   yd        Yards
   m         Meters
   km        Kilometers
   mi        Miles (statute)
   nmi       Nautical miles
   LS        Light Seconds
   AU        Astronomical Units
   LY        Light Years (Based on Julian year of 365.25 days)
   pc        Parsecs

   ===========================================================================
*/

   function Length_to_Length ($LengthAndUnits, $ToUnits)
{
// Define conversion constants for internal use only.
// These constants could also be defined externally
// for universal use by any programs that need them.
// This function was designed to be transportable.
   $M_PER_A   = 0.0000000001;
   $M_PER_NM  = 0.000000001;
   $M_PER_UM  = 0.000001;
   $M_PER_MM  = 0.001;
   $M_PER_CM  = 0.01;
   $M_PER_IN  = 0.0254;
   $M_PER_FT  = 0.3048;
   $M_PER_YD  = 0.9144;
   $M_PER_M   = 1;
   $M_PER_KM  = 1000;
   $M_PER_MI  = 1609.344;
   $M_PER_NMI = 1852;
   $CLIGHT    = 299792458;
   $M_PER_AU  = 149597870700;
   $M_PER_LY  = 9460730472580800;
   $M_PER_PC  = 30856775876793114.6633;

// Parse input value and separate the numerical value from the symbol.
// There MUST be at least one space between the number and the symbol.
// FALSE is returned on error if no space character detected at all or
// the given numeric value is not a valid number.
   $w = trim($LengthAndUnits); if (strpos($w, " ") === FALSE){return FALSE;}

   list($x, $FromUnits) = preg_split("[ ]", preg_replace("/\s+/", ' ', $w));

   if (!is_numeric($x)) {return FALSE;}

// Get input units symbol.  Unit symbols are NOT case sensitive.
   $FromUnitsSymbol = strtoupper(trim($FromUnits));

// Determine which conversion factor to use
// according to the units symbol argument.

   switch ($FromUnitsSymbol)
  {
   case "A"  : $f = $M_PER_A;   break;
   case "NM" : $f = $M_PER_NM;  break;
   case "UM" : $f = $M_PER_UM;  break;
   case "U"  : $f = $M_PER_UM;  break;
   case "MM" : $f = $M_PER_MM;  break;
   case "CM" : $f = $M_PER_CM;  break;
   case "IN" : $f = $M_PER_IN;  break;
   case "FT" : $f = $M_PER_FT;  break;
   case "YD" : $f = $M_PER_YD;  break;
   case "M"  : $f = $M_PER_M;   break;
   case "KM" : $f = $M_PER_KM;  break;
   case "MI" : $f = $M_PER_MI;  break;
   case "NMI": $f = $M_PER_NMI; break;
   case "LS" : $f = $CLIGHT;    break;
   case "AU" : $f = $M_PER_AU;  break;
   case "LY" : $f = $M_PER_LY;  break;
   case "PC" : $f = $M_PER_PC;  break;
   default   : $f = FALSE;      break;
  }

// Error if conversion factor is FALSE, which
// means that the units symbol was not found
// in the table of valid symbols.
   if ($f === FALSE) {return $f;}

// Otherwise, compute the equivalent meters value
// by multiplying the value of the argument by
// the conversion factor $f.
   $m = $x * $f;

// *********************************************************
// AT THIS POINT INPUT VALUE HAS BEEN CONVERTED INTO METERS.
// NEXT STEP IS TO CONVERT THE METERS INTO THE OUTPUT UNITS.
// *********************************************************

// Read the output units symbol argument.  The unit symbols
// are NOT case sensitive.
   $ToUnitsSymbol = strtoupper(trim($ToUnits));

// Determine the conversion factor to use from the units
// symbol argument.

   switch ($ToUnitsSymbol)
  {
   case "A"  : $f = $M_PER_A;   break;
   case "NM" : $f = $M_PER_NM;  break;
   case "UM" : $f = $M_PER_UM;  break;
   case "U"  : $f = $M_PER_UM;  break;
   case "MM" : $f = $M_PER_MM;  break;
   case "CM" : $f = $M_PER_CM;  break;
   case "IN" : $f = $M_PER_IN;  break;
   case "FT" : $f = $M_PER_FT;  break;
   case "YD" : $f = $M_PER_YD;  break;
   case "M"  : $f = $M_PER_M;   break;
   case "KM" : $f = $M_PER_KM;  break;
   case "MI" : $f = $M_PER_MI;  break;
   case "NMI": $f = $M_PER_NMI; break;
   case "LS" : $f = $CLIGHT;    break;
   case "AU" : $f = $M_PER_AU;  break;
   case "LY" : $f = $M_PER_LY;  break;
   case "PC" : $f = $M_PER_PC;  break;
   default   : $f = FALSE;      break;
  }

// Error if conversion factor is FALSE, which
// means that the units symbol was not found
// in the valid symbols table.
   if ($f == FALSE) {return FALSE;}

// Otherwise, return the equivalent of the given
// meters value by dividing the value of the
// argument by the conversion factor $f.
   return $m / $f;

} // End of  Length_To_Length(...)







/*
   ------------------------------------------------------------------
   This function computes the arbitrary-precision Nth root of a given
   numerical string X, rounded to any specified number of decimals.

   ARGUMENTS:
   N = Root = Integer 2, 3, 4, ..., etc.
   X = Numerical argument string for which we seek the Rth root

   This function is based on the master root iteration formula.

   MASTER ROOT ITERATION FORMULA

   Let
   N = Root to iterate (1, 2=Square root, 3=Cube root, etc. ...)
   a = Current approximation to Rth root of X
   b = Next approximation descended from a



   Given (N, X, a):

   n = N-1
   START:
   b = (a + X/(a^n)) / n

   For next approximation
   a = b
   Repeat from START until required accuracy reached.

   -----------------------------------------------------------------
   NumDecimals = Number of decimals at which to round off the result
                 if exact resolution is not possible.

   ERRORS:
   NO special error checking is done in this version of the function.
   ------------------------------------------------------------------
*/

   function bcNth_Root_Of_X ($N, $X, $decimals=16)
{
   $a = sprintf("%1.16f", pow($X, 1/$N)); // Compute first approximation
   $n = $N - 1;
   $d = abs(intval($decimals));
   $D = $d + 8;

   for ($i=1;  $i <= 50;   $i++)
  {
   $b = bcdiv(bcadd(bcmul($n,$a,$D),bcdiv($X, bcpow($a,$n,$D),$D),$D),$N,$D);

   if (bccomp($a,$b,$D) == 0) {break;} else {$a = $b;}
  }
   return rtrim(rtrim(bcadd($b, "0." . str_repeat(0,$d) . "5", $d), "0"), ".");
}





/*
   ===========================================================================
   This function returns the arbitrary-precision factorial of any non-negative
   integer value.  This function should be used with caution, since factorial
   values can be extremely large for relatively small arguments.  It may take
   several seconds to compute extremely large factorials (e.g. > 10000!), so
   a safety limit of 9999 is built in, but it can be easily removed.

   Input argument must be a positive integer.  Any decimal argument will be
   silently truncated to an integer, without rounding, rather than causing
   an error.

   ERRORS:
   FALSE is returned on error, if argument is non-numeric, negative or the
   argument exceeds the maximum set limit (default = 9999).
   ===========================================================================
*/

   function bcN_Factorial ($N=0)
{

// Error if argument N is non-numeric, negative or > 9999.
   if (!is_numeric($N) or $N < 0 or $N > 9999) {return FALSE;}

// Truncate any decimal argument to an
// integer value, without rounding.
   $n = bcadd($N, 0);

// Execute loop to compute N factorial product (P).
   $P=1;  for ($i=1;  $i <= $n;  $i++) {$P = bcmul($P, $i);}

// Done.
   return $P;
}




/*
  ========================================================================
  This function returns the permutations of (N) items taken (R) at a time.
  Like the digits in a number, the sequential order of items is important.

  Input arguments must be positive integers.  Any decimal argument will be
  truncated to an integer, without rounding, rather than returning an error.

   ERRORS:
   FALSE is returned on error, if either argument is non-numeric or out
   of valid range.
  ========================================================================
*/

  function bcPerm_N_R ($IntNStr, $IntRStr)
{
// Read numeric string arguments. Must be
// non-negative integer strings only. Any
// decimal part will be ignored and lost.
   $N = trim($IntNStr);
   $R = trim($IntRStr);

// Error if either argument is non-numeric.
   if (!is_numeric($N) or !is_numeric($R)) {return FALSE;}

// Error if either argument is out of valid range.
   if ($N < 0 or $R < 0 or $R > $N) {return FALSE;}

// Truncate argument(s) to integer value(s) without rounding.
   $N = bcadd($N, 0);  $R = bcadd($R, 0);

// Execute loop to compute P(N, r).
   $P=1;  for ($i=0;  $i < $R;  $i++) {$P = bcmul($P, $N-$i);}

// Done.
  return $P;
}




/*
  ========================================================================
  This function returns an arbitrary precision count of all total possible
  combinations or mixtures of (N) items taken (R) at a time.  Like a hand
  dealt in a poker game, the sequential order in which the cards were
  received is NOT important, but only which cards are held in the hand,
  regardless of their arrangement or order.

  ERRORS:
  FALSE is returned on error, if either argument is non-numeric or out
  of valid range.
  ========================================================================
*/

  function bcComb_N_R ($N, $R)
{
// Error if either argument is non-numeric.
   if (!is_numeric($N) or !is_numeric($R)) {return FALSE;}

// Truncate argument(s) to integer value(s) without rounding.
   $N = bcadd($N, 0);
   $R = bcadd($R, 0);

// Error if either argument is out of valid range.
   if ($N < 0 or $R < 0 or $R > $N) {return FALSE;}

// Execute loop to compute C(N, r).
   $C=1;  for ($i=0;  $i < $N-$R;  $i++) {$C = bcdiv(bcmul($C, $N-$i), $i+1);}

// Done.
   return $C;
}












/*
  ========================================================================
  This function returns the GCD (Greatest Common Divisor) of any two
  arbitrary-precision integers.  The GCD value will always be a
  positive value from 1 up to the lesser of the absolute values
  of the two integer arguments (A, B).

  Any numerical signs (±) are ignored.

  ERRORS:
  FALSE is returned on error if either argument is non-numeric.
  ========================================================================
*/

/*
   =========================================================================
   This function returns the greatest common divisor for any two integer
   value strings.  The numerical signs of the numbers are ignored.

   Arbitrary-precision arithmetic is used, so the integers should be given
   as strings, like "1234", so as not to be mistaken for standard floating-
   point values.

*/

   function bcGCD ($IntAStr, $IntBStr)
{
// Read integer string arguments.
   $A = trim($IntAStr);
   $B = trim($IntBStr);

// ERROR if either argument is non-numeric.
   if (!is_numeric($A) or !is_numeric($B)) {return FALSE;}

// Remove any negative signs and work with absolute values.
   $A = str_replace("-", "", $A);
   $B = str_replace("-", "", $B);

// Truncate any non-integer to an integer value WITHOUT rounding.
// This means that anything following the decimal point will
// simply be ignored and lost without rounding the integer part.
   $A = bcadd($A,0);     $B = bcadd($B,0);

// Return 1 if either integer equates to zero.
   if (bccomp($A,0) == 0 or bccomp($B,0) == 0) {return 1;}

// Swap (A,B) values, if necessary, to ensure that B >= A initially.
   if (bccomp($B,$A) < 0) {$x = $B;     $B = $A;     $GCD = $x;}
   else {$GCD = $A;}

// Compute and return the final GCD of the given arguments.
   while ($B != 0) {$x = $B;     $B = bcmod($GCD, $B);     $GCD = $x;}

   return $GCD; // Done

} // End of  bcGCD(...)



/*
   ========================================================================
   This PHP function performs arbitrary-precision
   integer fraction addition without reduction.

   (A/B) + (C/D) = E/F
   ========================================================================
*/

   function bcAdd_Frac ($A_B, $C_D)
{
   list($A,$B) = preg_split("[\/]", preg_replace("/\s+/", '', trim($A_B)));
   list($C,$D) = preg_split("[\/]", preg_replace("/\s+/", '', trim($C_D)));

   $E = bcadd(bcmul($A,$D), bcmul($B,$C));
   $F = bcmul($B,$D);

   return "$E/$F";
}





/*
   ========================================================================
   This PHP function performs arbitrary-precision
   integer fraction subtraction without reduction.

   (A/B) - (C/D) = E/F
   ========================================================================
*/

   function bcSub_Frac ($A_B, $C_D)
{
   list($A,$B) = preg_split("[\/]", preg_replace("/\s+/", '', trim($A_B)));
   list($C,$D) = preg_split("[\/]", preg_replace("/\s+/", '', trim($C_D)));

   $E = bcsub(bcmul($A,$D), bcmul($B,$C));
   $F = bcmul($B,$D);

   return "$E/$F";
}



/*
   ========================================================================
   This PHP function performs arbitrary-precision
   integer fraction multiplication without reduction.

   (A/B) * (C/D) = E/F
   ========================================================================
*/

   function bcMul_Frac ($A_B, $C_D)
{
   list($A,$B) = preg_split("[\/]", preg_replace("/\s+/", '', trim($A_B)));
   list($C,$D) = preg_split("[\/]", preg_replace("/\s+/", '', trim($C_D)));

   $E = bcmul($A,$C);     $F = bcmul($B,$D);

   return "$E/$F";
}




/*
   ========================================================================
   This function performs arbitrary-precision
   integer fraction division without reduction.

   (A/B) / (C/D) = (E/F)
   ========================================================================
*/

   function bcDiv_Frac ($A_B, $C_D)
{
   list($A,$B) = preg_split("[\/]", preg_replace("/\s+/", '', trim($A_B)));
   list($C,$D) = preg_split("[\/]", preg_replace("/\s+/", '', trim($C_D)));

   $E = bcmul($A,$D);     $F = bcmul($B,$C);

   return "$E/$F";
}

/*
   ========================================================================
   This function reduces arbitrary-precision integer fraction (A/B)
   to its lowest terms (a/b), if possible.

   Argument must be an integer fraction string in 'A/B' format.

   ERRORS:
   No special error checking is done.
   ========================================================================
*/

   function bcReduce_Frac ($A_B)
{
// -----------------------------------------------
// Extract the individual fraction elements (A, B)
// from the input argument string ("A/B").

   list($A,$B) = preg_split("[\/]", str_replace(' ','', trim($A_B)));

// -------------------------------------------------
// To find the GCD, we work with the absolute values
// of E and F, calling those working values wE and
// wF respectively.

   $wA = bcmul(((bccomp($A, 0) < 0)? -1:1), $A);
   $wB = bcmul(((bccomp($B, 0) < 0)? -1:1), $B);

   if (bccomp($wA,$wB) > 0) {$w=$wA;   $wA=$wB;   $wB=$w;}

// -------------------------------------
// Equate 1st approximation of GCD to wA

   $GCD = $wA;

// ------------------------------------------------
// Perform Euclid's algorithm loop while (wB != 0).
// The value of wB decreases after each cycle and
// eventually reaches zero, at which point we have
// reached the GCD value.  This will always equate
// to at least 1.

   while (bccomp($wB, 0) != 0)
         {$w=$wB;     $wB=bcmod($GCD, $wB);     $GCD=$w;}

// ------------------------------------------
// Compute and return reduced fraction (a/b).
// If GCD == 1, then the fraction cannot be
// reduced and is returned unchanged.

   $a = bcdiv($A, $GCD);
   $b = bcdiv($B, $GCD);

   return "$a/$b";

} // End of  bcReduce_Frac (...)




/*
   This function adds a string of BC decimal values together.
   The individual values are space delimited integers or
   fractional values.

   RetModeStr = Return mode (D = Decimal fraction  or  I = Integer fraction)

   String of Numbers = "1 2 1/3 4 5 6 7 8 9 10 11"
   decimals = 32
   sum  = 63.33333333333333333333333333333333

*/
   function bcAdd_Num_String ($StringOfNumbers, $RetModeStr="Dec", $decimals=16)
{
// ------------------------------------------------------------
// Read argument string of numerical expressions.  These can be
// integers or fractions, including decimal values.

   $w = preg_replace("/\s+/", " ", trim($StringOfNumbers));

// ------------------------------------------------
// Create 'rounding mask' to apply for rounding off
// the final result to the given number of decimals.

   $RoundingMaskStr = "0." . str_repeat("0", $decimals) . "5";

// -------------------------------------------------
// Read return mode argument and force to uppercase.
// Only 1st character is read (Dec or Int fraction).

   $RModeStr = substr(strtoupper(trim($RetModeStr)), 0,1);

// -----------------------------------
// Store expressions in working array.

   $NumsArray = preg_split("[ ]", $w);

// ----------------------
// Count the expressions.

   $NumsCount = count($NumsArray);

// ----------------------------------------------
// Cycle through the expressions, converting them
// all into exactly equivalent integer fractions.

   $sum = "0/1";

   for($i=0;   $i < $NumsCount;   $i++)
  {
// Get current number from work array and then
// convert any decimal fraction into integer
// fraction.
   $w = bcDec_to_Int_Frac ($NumsArray[$i]);

// Split current sum into A,B and current
// fraction to C,D and
   list($A,$B) = preg_split("[\/]", $sum);
   list($C,$D) = preg_split("[\/]", $w);

// Compute new A/B to replace previous A/B and
// repeat until all numbers have been summed.
   $AD = bcmul($A,$D);   $BC = bcmul($B,$C);   $BD = bcmul($B,$D);

   $A = bcadd($AD,$BC);  $B = $BD;

   $sum = "$A/$B"; // Dec_to_Int_Frac("$A/$B");
  }

// Reduce fraction to its lowest
// terms if possible.
// $sum = Dec_to_Int_Frac("$A/$B");

 $NegSign = "";


  if ($RModeStr == "D")
 {
  list($A,$B) = preg_split("[\/]", $sum);

  $sum = bcdiv($A,$B, $decimals);

// Temporarily remove any negative sign (-).  It will
// be re-attached to the returned result.
   if (substr($sum,0,1) == "-")
      {
       $NegSign = "-";   $sum = substr($sum, 1, strlen($sum));
      }

// Round off absolute sum value to given number of decimals
// and then trim off any redundant zeros and/or decimal point.
   $sum = rtrim(rtrim(bcadd($sum, $RoundingMaskStr, $decimals), "0"), ".");
 }
// Reattach any negative sign.  Done.
   return $NegSign . $sum;

} // End of  bcAdd_Num_String(...)







/*
   This function multiplies a string of BC decimal values together.
   The individual values are space delimited integers or fractional
   values in any combination.

   RetModeStr = Return mode (D = Decimal fraction  or  I = Integer fraction)

   PROD

    A     C     A*C     E
   --- * --- = ----- = ---
    B     D     B*D     F


   1/3.15  2  2.90/1.3  4  5  6  7  8  9  10  13  10  11
   =
   1/3.15 * 2 * 2.90/1.3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 13 * 10 * 11

*/
   function bcMul_Num_String ($StringOfNumbers, $RetModeStr="Dec", $decimals=16)
{
// Read argument string of numerical expressions.  These can be
// integers or fractions, including decimal values.
   $w = preg_replace("/\s+/", " ", trim($StringOfNumbers));

// Create 'rounding mask' to apply for rounding off
// the final result to the given number of decimals.
   $RoundingMaskStr = "0." . str_repeat("0", $decimals) . "5";

// Read return mode argument and force to uppercase.
// Only 1st character is read (Dec or Int fraction).
   $RModeStr = substr(strtoupper(trim($RetModeStr)), 0,1);

// Store expressions in working array.
   $NumsArray = preg_split("[ ]", $w);

// Count the expressions.
   $NumsCount = count($NumsArray);

// Cycle through the expressions, converting them
// all into exactly equivalent integer fractions.
   $prod = "1/1";

   for($i=0;   $i < $NumsCount;   $i++)
  {
// Get current number from work array and then
// convert any decimal fraction into integer
// fraction.
   $w = Dec_to_Int_Frac ($NumsArray[$i]);

// Split current product into A,B and current
// working number (w) string into to C,D
   list($A,$B) = preg_split("[\/]", $prod);
   list($C,$D) = preg_split("[\/]", $w);

// Compute new A/B to replace previous A/B and
// repeat until all numbers have been multiplied.
   $AC = bcmul($A,$C);   $BD = bcmul($B,$D);

   $A = $AC;  $B = $BD;

   $prod = "$A/$B";
  }

// Reduce fraction to its lowest
// terms if possible.
 $prod = Dec_to_Int_Frac("$A/$B");

 $NegSign = "";


  if ($RModeStr == "D")
 {
  list($A,$B) = preg_split("[\/]", $prod);

  $prod = bcdiv($A,$B, $decimals);

// Temporarily remove any negative sign (-).  It will
// be re-attached to the returned result.
   if (substr($prod,0,1) == "-")
      {
       $NegSign = "-";   $prod = substr($prod, 1, strlen($prod));
      }

// Round off absolute product value to given number of decimals
// and then trim off any redundant zeros and/or decimal point.
   $prod = rtrim(rtrim(bcadd($prod, $RoundingMaskStr, $decimals), "0"), ".");
 }
// Reattach any negative sign.  Done.
   return $NegSign . $prod;

} // End of  bcMul_Num_String(...)







/*
   ========================================================================
   This function will convert an arbitrary precision positive base 10
   X value into its equivalent in any base from 2 to 36.  The X value
   is NOT restricted to integers and also allows fractional values.

   ARGUMENTS:
   $X10 = Base 10 numeric string to be to converted
          into its BaseB equivalent.

   $BaseB = Base (2 to 36) in which to interpret input digit string.

   ERRORS:
   On error FALSE is returned.  An error occurs if either
   of the arguments is non-numeric or out of valid range.

   This is a level 2 function, dependent on:
   bcBase10_Int_To_BaseB (...)

   ========================================================================
*/

   function bcBase10_To_BaseB ($X10, $BaseB, $TruncationLimit=0)
{
   $p = 1000; // Decimals precision control factor (Don't ask).

   $q =  $TruncationLimit; // Decimal truncation limit.

   $X = trim($X10);  $sign = (substr($X,0,1) == "-")? "-" : "";
   $X = str_replace("-","", $X);

   if (strpos($x, ".") === FALSE){$X .= ".0";}

   list($i, $f) = preg_split("[\.]", $X);  $f = "0.$f";

   $IntPartOut = bcBase10_Int_To_BaseB ($i, $BaseB);

   $w = bcmul(bcPow($BaseB, $p), $f);

   $fp = bcBase10_Int_To_BaseB ($w, $BaseB);

   $zeros = str_repeat("0", $p - strlen($fp));

   if ($q > 0) {$fp = substr($fp, 0, $q-0);}

   $FracPartOut = ".$zeros$fp";

   $FracPartOut = rtrim(rtrim($FracPartOut,'0'), '.');

   return "$sign$IntPartOut$FracPartOut";

} // End of  bcBase10_To_BaseB(...)





/*
   ========================================================================
   This function will convert an arbitrary precision number value in
   any base from 2 to 36 into its equivalent in base 10.  It is NOT
   restricted only to integers and applies to fractional values as well.

   ARGUMENTS:
   $BaseBIntStr = Base B numeric string to be to converted
                  into its base 10 equivalent.

   $BaseB = Base of input digit string (2 to 36).

   ERRORS:
   On error FALSE is returned.  An error occurs if either
   of the arguments is non-numeric or out of valid range.

   This is a level 2 function.
   ========================================================================
*/


   function bcX_Base_B_To_Base_10 ($BaseBNumStr, $BaseB)
{

// Compute base 10 decimal equivalent to BaseB fraction.

   $a = 0;
   $b = 1;

   $BaseDigits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

// Read and copy input arguments.
   $x = trim($BaseBNumStr);

// Remember numerical sign, if any and work with absolute
// value.  The sign is restored to output value.
   $sign = (substr($x, 0,1) != "-")? '' : '-';
   $x = str_replace("-", '', $x);
   $x = str_replace("+", '', $x);

// If argument is an integer value, then attach ".0" to it.
   if(strpos($x, ".") === FALSE) {$x .= ".0";};

// Get only integer part of the BaseB numeric string.
   $xi = substr($x, 0, strpos($x, "."));

// Compute equivalent base 10 integer value.
   $IntegerPartBase10 = bcBase_B_Int_To_Base_10($xi, $BaseB);

// Get only the fractional digits of the BaseB numeric string.
   $xf = substr($x, strpos($x, ".")+1, strlen($x));

// Count BaseB fractional digits.
   $DigitsCount = strlen($xf);

// Cycle through each digit, creating integer fraction terms
// and summing them up to final a/b.

   $out = '';

   $PowB = $BaseB;

   for($i=0;   $i < $DigitsCount;   $i++)
   {
// get digit and convert to base 10 equivalent.
   $c = strpos($BaseDigits, substr($xf, $i, 1));
   $d = $PowB;  $PowB = bcmul($PowB, $BaseB);

// Add to current (a/b).
   $a = bcadd(bcmul($a,$d), bcmul($b,$c));
   $b = bcmul($b,$d);

//   $out .= "$c/$d\n";

   }

// Compute final raw, unreduced (a/b) value.
   $c = $IntegerPartBase10;
   $d = "1";
   $a = bcadd(bcmul($a,$d), bcmul($b,$c));
   $b = bcmul($b,$d);

//   return $out;

   $w = bcreduce_frac("$sign$a/$b");

   $w = preg_replace("/\s+/", '', trim($w)); // Modified vesrion

// If single value, then put over 1.
   if (strpos($w, "/") === FALSE) {$w .= "/1";}

// Get numerator and denominator values.
   list($A,$B) = preg_split("[\/]", $w);

// Compute decimal value equivalent truncated
// at 1000 decimals.
   $w = bcdiv($A,$B, 1000);

   return rtrim(rtrim($w,'0'), '.');

} // End of  bcXBaseB_To_Base10(...)




/*
   ========================================================================
   This boolean function tests to see of the given argument is a valid
   BaseB (2 to 36) number.

   If valid, then TRUE is returned, otherwise FALSE is returned.

   NOTE:
   It is possible for a BaseB number to be valid in more than one base.

   A given number is considered valid only if none of its digits are
   invalid for the given base.  For example, a base 10 number cannot
   contain the digit 'F', but a hexadecimal (base 16) number can.

*/

   function bcIs_Base_B ($XBaseBString, $BaseB)
{
// Read numeric work string and remove any decimal or numerical sign.
   $X = str_replace(".", '', strtoupper(trim($XBaseBString)));
   $X = str_replace("-", '', $X);
   $X = str_replace("+", '', $X);

   $DigitsSpectrum = substr("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, $BaseB);

// Cycle through every digit.  Invalid if any digit is not found
// in DigitsSpectrum string, so return FALSE.
// Otherwise, TRUE is returned.
   $DigitsCount = strlen($X);
   for ($i=0;   $i < $DigitsCount;   $i++)
       {
        $digit = substr($X, $i, 1);
        if (strpos($DigitsSpectrum, $digit) === FALSE) {return FALSE;}
       }
   return TRUE;

} // End of  bcIs_Base_B(...)




/*
   ========================================================================
   This function will convert an arbitrary precision integer
   in any base from 2 to 36 into its equivalent in base 10.

   ARGUMENTS:
   $XBaseBString = Base B integer string to be converted
            into its base 10 equivalent.

   $FromBase = Base of input digit string (2 to 36).

   ERRORS:
   On error, FALSE is returned if either argument is invalid.

   ========================================================================
*/

   function bcBaseB_Int_To_Base10 ($XBaseBString, $BaseB)
{

// Read input arguments.
   $Xb = strtoupper(trim($XBaseBString));
   $B  = trim($BaseB);

// ERROR if base is outside of range 2 to 36.
   if ($B < 2 or $B > 36) {return FALSE;}

// Remember any numerical sign and work with absolute value.
// The numerical sign will be restored at the end.
   $sign = (substr($Xb, 0,1) == '-')? '-' :'';
   if ($sign == '-') {$Xb = substr($Xb, 1, strlen($Xb));}

// Count digits in base b integer X.
   $XbDigitsCount = strlen($Xb);

// Initialize base 10 summation accumulator
// and initial power (= 1) of the base.
   $X10 = 0;
   $PowB = 1;

// Define the valid digits spectrum for the given base.
   $DigitsSpectrum = substr("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, $B);

// Perform loop to compute the base 10
// equivalent of the base b integer Xb.
   for ($i=0;   $i < $XbDigitsCount;   $i++)
       {
//      Get current base b digit from Xb.
        $db = substr($Xb, $XbDigitsCount - $i - 1, 1);

//      Get base 10 numerical value (= String index) of digit.
        $d10 = strpos($DigitsSpectrum, $db);

//      If FALSE is returned, it means that the base b digit was
//      not found within the valid base b DigitsSpectrum string.
        if ($d10 === FALSE) {return FALSE;}

//      Compute and accumulate value of current base 10 term.
        $X10 = bcadd($X10, bcmul($PowB, $d10));

//      Update power of base for next cycle.
        $PowB = bcmul($PowB, $B);
       }

// Done.
   return "$sign$X10";

} // End of  bcBaseB_Int_To_Base10(...)



/*
   ========================================================================
   This function will convert an arbitrary precision base 10 integer into
   its equivalent in any given base from 2 to 36.

   ARGUMENTS:
   $x10IntStr = Base 10 integer string to be to converted
                into its Base B equivalent.

   $BaseB = Base of converted output (2 to 36).

   ERRORS:
   On error, FALSE is returned if either argument is non-numeric
   or out of valid range.
   ========================================================================
*/

   function bcBase10_Int_To_BaseB ($X10IntStr, $BaseB)
{
// Error if either argument is non-numeric
// or if base of out of range (2 to 36).

   if (!is_numeric($X10IntStr) or !is_numeric($BaseB)) {return FALSE;}

   if ($BaseB < 2 or $BaseB > 36) {return FALSE;}

// Define available digits spectrum for bases 2 to 36.
   $DigitsSpectrum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

   $XB = "";
   $X10 = trim($X10IntStr);

// Remember any numerical sign and work with absolute value.
// The numerical sign will be restored at the end.
   $sign = (substr($X10, 0,1) == '-')? '-' :'';
   if ($sign == '-') {$X10 = substr($X, 1, strlen($X10));}

   if (bccomp($X10, 0) == 0) {return "0";} // Handle zero argument.

// Perform base 10 to base B conversion loop, digit-by-digit.
   while (bccomp($X10, 0) > 0)
  {
   $XB = substr($DigitsSpectrum, bcmod($X10, $BaseB), 1) . $XB;
   $X10 = bcdiv($X10, $BaseB);
  }
// Done.
   return "$sign$XB";

}  // End of  bcBase10_Int_To_BaseB(...)




/*
   This function converts a single integer or decimal fraction string
   into the equivalent fraction in pure integer form (A/B).

   NOTE: Numbers must be in standard form, NOT scientific E-notation.

   "12.042/3.71" would become "12042/3710"

   "1.5000/0.17" would become "150/17"

   "1.234" would become "1234/1000"

   "-0.02337/11.7346709" would become "-233700/117346709"

*/
   function bcDec_to_Int_Frac ($FractionString)
{
// Read fraction string argument and remove ALL spaces.
   $FracStr = str_replace(" ", "", trim($FractionString));

// If only a single value, put it over 1 (like A/1).
   if (strpos($FracStr, "/") === FALSE)  {$FracStr = "$FracStr/1";}

// Split fraction into numerator and denominator elements.
   list($numer, $denom) = preg_split("[\/]", $FracStr);

// If integer (no decimal) part, then append .0 to it.
   $numer .= (strpos($numer, ".") === FALSE)? ".0":"";
   $denom .= (strpos($denom, ".") === FALSE)? ".0":"";

// If any decimal numbers start with a sign followed by a decimal point
// or start with only a decimal point, then a zero must be inserted be-
// fore the decimal.  For example, -.125 would become -0.125 instead.
   $numer = str_replace("-.", "-0.", $numer);
   $numer = str_replace("+.", "+0.", $numer);
   if (substr($numer,0,1) == ".") {$numer = str_replace(".", "0.", $numer);}
   $denom = str_replace("-.", "-0.", $denom);
   $denom = str_replace("+.", "+0.", $denom);
   if (substr($denom,0,1) == ".") {$denom = str_replace(".", "0.", $denom);}



// Isolate only the decimal parts of numerator and denominator.
// This means everything from the decimal point and after.
   $NumerDec = strrchr($numer, '.');
   $DenomDec = strrchr($denom, '.');

// Length of longest decimal part determines power of
// 10 to be used as the common multiplier to replace
// the decimal values with integers.
   $Pow10 = bcpow("10", max(strlen($NumerDec)-1, strlen($DenomDec)-1));

// Multiply both numerator and denominator by
// the common power of 10 needed to convert
// them both into integer values.
   $numer = bcmul($Pow10, $numer);
   $denom = bcmul($Pow10, $denom);

// Remove any redundant common zeros from numerator and denominator.
   while(substr($numer, -1) == "0" and substr($denom, -1) == "0")
  {
   $numer = substr($numer, 0, strlen($numer)-1);
   $denom = substr($denom, 0, strlen($denom)-1);
  }

// Reduce to lowest integer terms, if possible.
// Otherwise, finished.
   $A = $numer;
   $B = $denom;

// If negative A or B argument, then convert to absolute
// value. Any numerical signs are irrelevant and removed.
   if (substr($A,0,1) == "-"){$A = substr($A,1,strlen($A));}
   if (substr($B,0,1) == "-"){$B = substr($B,1,strlen($B));}

// Swap working (A, B) values, if necessary,
// so that A > B initially.
   if (bccomp($A, $B) < 0)
      {
       $w   = $B;
       $B   = $A;
       $GCD = $w;
      }
   else
      {
       $GCD = $A;
      }

// Compute the GCD, if any, of the given arguments.  This value
// is used to reduce the integer fraction to its lowest terms.
   while ($B != 0)   {$w = $B;    $B = bcmod($GCD, $B);    $GCD = $w;}

// Now, reduce fraction to lowest terms
// by dividing both terms by their GCD.
   $numer = bcdiv($numer, $GCD);
   $denom = bcdiv($denom, $GCD);

// Done.
   return "$numer/$denom";

} // End of  Dec_to_Int_Frac(...)



















/*
   ========================================================================
   This function performs LaGrange interpolation within an XY data table.

   ========================================================================
*/

   function LaGrange_Interp ($XYDataTable, $xArg)
{

// -----------
// Initialize.

   $XDataStr = $YDataStr = '';

// --------------------------------------------------------------
// Read and split XY data pairs into a work array.  In the array,
// even number indices = X-data, odd number indices = Y-data.

   $XY = preg_split("[ ]", preg_replace("/\s+/", ' ', trim($XYDataTable)));

// ---------------------------------------------
// Count the total number of data elements. This
// value should always be an even number.

   $TotalDataCount = count($XY);

// ------------------------------------------------------------
// Number of data pairs.  This value should be an integer value
// exactly equal to 1/2 the total number of data points. If not,
// then there is an odd mismatched data point.

   $n = $TotalDataCount / 2;

// -----------------------------------------------------------
// Return error message if data vector element count mismatch.
// For every X value there must be a corresponding Y value or
// an XY data count mismatch error occurs.  An error will also
// occur if insufficient data.  There must be at least two XY
// data points given.

   if ($TotalDataCount < 4 )
      {return "ERROR: There must be at least two XY data pairs.";}

   if ($n != floor($n + 0.5))
      {return "ERROR: XY Data Count Mismatch. Odd data element.";}

// ------------------------------------------------------------
// Compute number of XY data pairs.  This value is exactly half
// the value of the total number of data elements.

   $n = $TotalDataCount / 2;

// -------------------------------------------------------
// Construct separate XY data strings from the array data.
// The XY data strings should each contain the same number
// of data elements.

   for($i=0;   $i < $TotalDataCount;   $i+=2)
      {
       $XDataStr .= $XY[$i]   . " ";
       $YDataStr .= $XY[$i+1] . " ";
      }

// --------------------------------------------------------------
// Split the created XY data vector strings into matching indexed
// arrays.  For every X value there must be a matching Y value
// and no two X values can be identical.

   $X = preg_split("[ ]", trim($XDataStr));
   $Y = preg_split("[ ]", trim($YDataStr));

// ----------------------------------------
// Read X argument for which to interpolate
// the Y value from the given XY data.

   $x = trim($xArg);  if ($x == "") {$x = "0";}

// -----------------------------------
// Initialize Y summation accumulator.
   $y = 0.0;

// -----------------------------------------------------
// Compute Lagrangian product (Li) for given X argument.

   for ($i=0;   $i < $n;   $i++)
       {
        $Li = 1.0;

        for ($j=0;   $j < $n;   $j++)
            {
             if ($j != $i) // Skip this cycle when j == i
                {
                 $Li = ($Li * ($x - $X[$j])) / ($X[$i] - $X[$j]);
                }
            } // Next j

//      --------------------------------------
//      Accumulate sum of Yi polynomial terms.

        $y += ($Y[$i] * $Li);

       } // Next i

   return $y;

} // End of  LaGrange_Interp(...)




/*
   ========================================================================
   This function simply swaps all matching XY data pairs held in a
   space delimited string argument. It was designed as a companion
   to the Lagrange_Interp() function to easily facilitate the easy
   swapping of the XY data computation roles.

   The data trend can be linear or non-linear, however
   only 2 data pairs are needed for any linear trend.

   ERRORS:
   On error FALSE is returned.  An error occurs if
   odd number of data elements or < 4 elements.
   ========================================================================
*/

   function Swap_XY_Data_Columns ($XYDataStr)
{

// -------------------------------------------------------------------------
// Read space-delimited data string and split items into a sequential array.
   $XYArray = preg_split("[ ]", preg_replace("/\s+/", " ", trim($XYDataStr)));

   $XYArrayCount = count($XYArray);

   if ($XYArrayCount % 2 != 0 or $XYArrayCount < 4) {return FALSE;}

   $XYPairsCount = $XYArrayCount / 2;

// -------------------------------------------------------
// Construct and return output string of swapped XY pairs.
   $YXDataPairsStr = "";

   for ($i=1;   $i < $XYArrayCount;   $i+=2)
       {
        $YXDataPairsStr .= $XYArray[$i] . " " . $XYArray[$i-1] . "\n";
       }

   return trim($YXDataPairsStr);
}








/*
  ----------------------------------------------------------------------
  This function returns the time of an extremum (minimum or maximum), if
  any, within a given time vs event table.  For example, this function
  can be used to find the times of perihelion, aphelion, perigee, apogee
  or any general periapsis or apoapsis times.  It is based on a 5-point
  data table and the extremum is computed from a polynomial derived from
  the given data.

  ERRORS:
  No error checking is done and the function assumes that an extremum
  exists within the given data table.

  Possible Uses:
  XY-Data = JD vs DistKm

  ----------------------------------------------------------------------
*/


   function Extremum_5 ($DataTableStr)
{

// ---------------------------------
// Read data table and parse values.

   $DataTable = preg_replace("/\s+/", ' ', trim($DataTableStr));

   list ($x1,$y1, $x2,$y2, $x3,$y3, $x4,$y4, $x5,$y5)
   = preg_split("[ ]", $DataTable);

   $interval = $x2 - $x1;

   $a = $y2 - $y1;
   $b = $y3 - $y2;
   $c = $y4 - $y3;
   $d = $y5 - $y4;

   $e = $b - $a;
   $f = $c - $b;
   $g = $d - $c;
   $h = $f - $e;
   $i = $g - $f;
   $j = $i - $h;

   $k = $j/24;
   $m = ($h + $i)/12;
   $n = $f/2 - $k;
   $p = ($b + $c)/2 - $m;

   $q = $r = 0;

   while ($r < 25)
  {
   $s = 6*($b + $c) - $h - $i + 3*$q*$q*($h + $i) + 2*$q*$q*$q*$j;
   $t = $j - 12*$f;
   $q = $s/$t;

   $r++;
  }
   return $q * $interval + $x3;

} // End of  Extremum5(...)



/*
   INTERPOLATES THE TIME IN HOURS GIVEN AN INTERPOLATION TABLE
   AND JDTT ARGUMENT. FOR ASTRONOMY, THIS CAN BE USED TO INTER-
   POLATE THE R.A. FOR A SPECIFIED JDTT ARGUMENT.
*/
   function Interp_Hours ($HoursInterpTableStr, $JDTTArg)
{
   $wArray = preg_split("[\n]", trim($HoursInterpTableStr));
   $wCount = count($wArray);

   $JDArg = floatval($JDTTArg);

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

   list($CurrJDTT, $CurrRAHr) = preg_split("[ ]", $CurrLine);
   list($PrevJDTT, $PrevRAHr) = preg_split("[ ]", $PrevLine);

   $diff = $CurrRAHr - $PrevRAHr;

   if ($diff < 0) {$CurrRAHr += 24;}

   $wArray[$i] = "$CurrJDTT $CurrRAHr\n";

  }

// CONSTRUCT FINAL INTERPOLATION TABLE READY FOR USE.
   $w = '';
   for ($i=0;   $i < $wCount;   $i++)
  {
   $w .= trim($wArray[$i]) . "\n";
  }
   $HoursInterpTableStr = strip_wspace($w);


// PERFORM INTERPOLATION FOR hours VALUE CORRESPONDING TO JDTT VALUE.
   $hours = Lagrange_interp($HoursInterpTableStr, $JDArg);

// IF hours > 24, THEN SUBTRACT 24.
   $hours -= ($hours > 24)? 24:0;

   return sprintf("%2.7f", $hours);

} // End of  Interp_Hours(...)





/*
   This function computes the standard deviation of a set of
   comma, semicolon or space-delimited numbers.

   $NumericDataString = String of delimited numeric values.
                        Example: "123.2  121.5  121.1  124.7"

   SampleMode = 0 or 1 = default.
   0 = Population sample randomly drawn from larger population
   1 = Sample is the total population. (= default)
*/

   function Std_Deviation ($NumericDataString, $SampleMode=1)
{
   $w = str_replace(",", " ", $NumericDataString);
   $w = str_replace(";", " ", $w);
   $w = preg_replace('/\s+/', ' ', trim($w));
   $WorkString = $w;

   $SampleMode = (abs(intval($SampleMode)) != 0)? 1:0;

   $N = $sum = 0;

   while ($WorkString != '')
  {
   $N++;

   $i = strpos($WorkString, ' ');

   if ($i === FALSE)
      {
       $sum  = $sum + $WorkString;
       $mean = $sum / $N;
       break;
      }

   $xi = trim(substr($WorkString, 0, $i));
   $sum += $xi;
   $mean = $sum / $N;

   $WorkString = trim(substr($WorkString, $i, strlen($WorkString)));
  }

   $N = $sum = 0;

   while ($w != '')
  {
   $N++;

   $i = strpos($w, ' ');

   if ($i === FALSE)
      {
       $diff = $w - $mean;
       $sum += ($diff * $diff);
       break;
      }

   $xi = trim(substr($w, 0, $i));
   $diff = $xi - $mean;
   $sum += ($diff * $diff);
   $w = trim(substr($w, $i, strlen($w)));
  }
   $variance = $sum / ($N - $SampleMode);

   return sqrt($variance);

} // End of  Std_Deviation(...)







/*
 ------------------------------------------------------------------------------
 This function computes the standard statistics for a numerical data string.
 The returned string contains the following computed statistics:
 n                  = Number of data elements (numbers to be processed)
 $sum               = Sum of all the data
 $mean              = Arithmetic mean of the data
 $SUMsquaresOfDiffs = Sum of the squares of the differences
 $variance0         = Population variance
 $variance1         = Sample variance
 $StdDev0           = Population standard deviation
 $StdDev1           = Sample standard deviation";
*/

   function Std_Dev_Stats ($NumericDataString)
{
   $w = str_replace(',',  ' ', $NumericDataString);
   $w = str_replace(';',  ' ', $w);
   $w = str_replace('\n', ' ', $w);
   $WorkString = preg_replace('/\s+/', ' ', trim($w));

   $n = $SUMdata = 0;

   while ($WorkString != '')
  {
   $n++;

   $i = strpos($WorkString, ' ');

   if ($i === FALSE)
      {
       $SUMdata += $WorkString;
       $mean = $SUMdata / $n;
       break;
      }

   $xi = trim(substr($WorkString, 0, $i));
   $SUMdata += $xi;
   $mean = $SUMdata / $n;

   $WorkString = trim(substr($WorkString, $i, strlen($WorkString)));
  }

// *************************************************************************
// Cycle through data computing the sum of squares of differences from mean.
// The mean of this summation of squares is the population variance.

   $w = str_replace(",", " ", $NumericDataString);
   $w = str_replace(";", " ", $w);
   $WorkString = preg_replace('/\s+/', ' ', trim($w));

   $n = $SUMsquaresOfDiffs = 0;

   while ($WorkString != '')
  {
   $n++;

   $i = strpos($WorkString, ' ');

   if ($i === FALSE)
      {
       $diff = $WorkString - $mean;
       $SUMsquaresOfDiffs += ($diff * $diff);
       break;
      }

   $xi = trim(substr($WorkString, 0, $i));
   $diff = $xi - $mean;
   $SUMsquaresOfDiffs += ($diff * $diff);
   $WorkString = trim(substr($WorkString, $i, strlen($WorkString)));
  }

   $variance0 = $SUMsquaresOfDiffs / ($n-0); // sigma
   $variance1 = $SUMsquaresOfDiffs / ($n-1); // s

   $StdDev0 = sqrt($variance0); // SqRt(sigma)
   $StdDev1 = sqrt($variance1); // SqRt(s)

   $StdDevStats = "$n $SUMdata $mean $SUMsquaresOfDiffs $variance0 $variance1 $StdDev0 $StdDev1";

   return $StdDevStats;

} // End of  Std_Dev_Stats(...)



/*
   ========================================================================
   Returns the compass symbol corresponding to azimuth angle in degrees and
   zero-mode.  The default zero azimuth direction is north.

   ZeroModeSymbol = 'N' (North = 0 convention = Default)
   ZeroModeSymbol = 'S' (South = 0 convention = Optional)

   ========================================================================
*/

   function Compass_Symbol ($AzimDeg, $ZeroModeSymbol='N')
{
   $a = floatval($AzimDeg) + ((strtoupper($ZeroModeSymbol) == 'S')? 180:0);

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

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

   if ($ZeroModeSymbol != 'S')
   {return trim(substr("N  NNENE ENEE  ESESE SSES  SSWSW WSWW  WNWNW NNWN", 3*$i, 3));}
   else
   {return trim(substr("S  SSWSW WSWW  WNWNW NNWN  NNENE ENEE  ESESE SSES", 3*$i, 3));}

} // End of  Compass_Symbol (...)



/*
  ========================================================================
  Computes the theoretical astronomical Delta-T estimate in seconds for
  any dates between 1600 and 2200, based on the NASA polymomial
  expressions for Delta T.

  NASA Ref:
  http://eclipse.gsfc.nasa.gov/LEcat5/deltatpoly.html


  ARGUMENTS:
  Y = Year number (1600 to 2200 AD)
  mNum = Month number(01 to 12)
  EXAMPLE: Ym = 195005 (= 1950 May)

  ERRORS:
  FALSE is returned on error.  An error results if either
  the year or month number is invalid.  A zero year value
  will return an error.
  ========================================================================
*/

   function Delta_T ($yyyymnOrJDTT, $RetMode=' ')
{
// ERROR if date value is non-numeric.
// It must be a positive 6-digit integer value.
// yyyymn = 100*Y + m
//
// Modifications.
// Can also use a 7-digit JD number.
// A JD number will be used to determine the year and month values.
//
// Now has a DMS return mode to save work.
// HMS = Return XXh XXm XXs
//  :  = Return hh:mm:ss

// --------------------------------------------
// Read return mode.  Empty = Default = Seconds
// This is CASE SENSITIVE
//  H = Return XXh XXm XXs
//  h = H.hhh
//  : = Return hh:mm:ss

   $RMode = substr(trim($RetMode), 0,1);  if ($RMode == '') {$RMode = ' ';}

   $dTSeconds = _Delta_T ($yyyymnOrJDTT);

   $DeltaT = sprintf("%+3.0f", round($dTSeconds, 0));

   if ($DeltaT === FALSE){return FALSE;;}

   if ($RMode == ':')
  {
   $dTHrs = $dTSeconds/3600;
   $DeltaT = Hours_to_HMS($dTHrs, 0, '+', ':');
  }

   if ($RMode == 'h')
  {
   $DeltaT = sprintf("%+1.9f", $dTSeconds/3600);
  }

   if ($RMode == 'H')
  {
   $dTHrs = $dTSeconds/3600;
   $DeltaT = Hours_to_HMS($dTHrs, 0, '+', $RMode);
  }

// Return Delta T in indicated mode.
   return $DeltaT;

} // End of  Delta_T(...)

/*
   This is a subroutine for the upper-level
   Delta T function above that calls it.
*/
   FUNCTION _Delta_T ($yyyymnOrJDTT)
{
// ERROR if date value is non-numeric.
// It must be a positive 6-digit integer value.
// yyyymn = 100*Y + m
//
// Modifications.
// Can also use a 7-digit JD number.
// A JD number will be used to determine the year and month values.
//
// Now has a DMS return mode to save work.
// HMS = Return XXh XXm XXs
//  :  = Return hh:mm:ss

// Read input argument.
   $w = abs(floatval(trim($yyyymnOrJDTT)));

// ERROR returns FALSE if argument is invalid value.
   if ($w == 0) {return FALSE;}

// If argument contains a decimal point, then it is assumed to be a
// Julian date value and is converted into the corresponding JD number.
   if (strpos($w, '.') !== FALSE) {$w = floor($w + 0.5);}

// AT THIS POINT, w SHOULD BE EITHER A 6 OR 7-DIGIT POSITIVE INTEGER.
//
// 6 digits = Integer-encoded date elements yyyymn = 100*yyyy + mn
// 7 digits = JD number corresponding to Julian Date argument value.

// ERROR returns FALSE if either argument is neither 6 or 7 digits.
   if (strlen($w) < 6 or strlen($w) > 7) {return FALSE;}


// HANDLE CASE OF JULIAN DATE OR JD NUMBER ARGUMENT.
// If 7-digit integer, then compute corresponding
// mn, dn, yyyy values. The returned dn is not used.
   if (strlen($w) == 7)
  {
   list($mn, $dn, $yyyy) = preg_split("[\/]", JDtoGregorian($w));

   $w = 100*$yyyy + $mn;
  }

// Otherwise, it is assumed that the argument is a 6-digit yyyymn value.
   $yyyymn = $w;

// Parse yyyymn value.

   $Y = substr($yyyymn, 0,4);
   $mNum = intval(substr($yyyymn, 4,2));

// ERROR if year value is invalid. It must
// be in the range from 1600 to 2200.

   if ($Y < 1600 or $Y == 0 or $Y > 2200) {return FALSE;}

// ERROR if month value is outside the range from 1 to 12.

   if ($mNum < 1 or $mNum > 12) {return FALSE;}

// If a negative (BC) year is given , it is converted into the
// corresponding mathematical year by adding 1.
//
// The year 100 BC (-100), becomes the mathematical year -99.
// This will never be the case here because this implementation
// of the polynomials is not meant to cover ancient dates that
// use negative year values, only years from 1600 AD onward.

   $y = $Y + (($Y < 0)? 1:0) + ($mNum - 0.5)/12;


   if ($Y >= 1600 and $Y < 1700)
      {
       $u  = $y - 1600;

       return 120 - 0.9808*$u - 0.01532*$u*$u + $u*$u*$u/7129;
      }


   if ($Y >= 1700 and $Y < 1800)
      {
       $u  = $y - 1700;
       $u2 = $u * $u;
       $u3 = $u * $u2;
       $u4 = $u * $u3;

       return 8.83 + 0.1603*$u
                   - 0.0059285*$u2
                   + 0.00013336*$u3
                   - $u4/1174000;
      }


   if ($Y >= 1800 and $Y < 1860)
      {
       $u  = $y - 1800;
       $u2 = $u * $u;
       $u3 = $u * $u2;
       $u4 = $u * $u3;
       $u5 = $u * $u4;
       $u6 = $u * $u5;
       $u7 = $u * $u6;

       return 13.72 - 0.332447*$u
                    + 0.0068612*$u2
                    + 0.0041116*$u3
                    - 0.00037436*$u4
                    + 0.0000121272*$u5
                    - 0.0000001699*$u6
                    + 0.000000000875*$u7;
      }


   if ($Y >= 1860 and $Y < 1900)
      {
       $u  = $y - 1860;
       $u2 = $u * $u;
       $u3 = $u * $u2;
       $u4 = $u * $u3;
       $u5 = $u * $u4;

       return 7.62 + 0.5737*$u
                   - 0.251754*$u2
                   + 0.01680668*$u3
                   - 0.0004473624*$u4
                   + $u5 / 233174;
      }


   if ($Y >= 1900 and $Y < 1920)
      {
       $u  = $y - 1900;
       $u2 = $u * $u;
       $u3 = $u * $u2;
       $u4 = $u * $u3;

       return 1.494119*$u - 2.79
                          - 0.0598939*$u2
                          + 0.0061966*$u3
                          - 0.000197*$u4;
      }


   if ($Y >= 1920 and $Y < 1941)
      {
       $u  = $y - 1920;
       $u2 = $u * $u;
       $u3 = $u * $u2;

       return 21.20 + 0.84493*$u - 0.076100*$u2 + 0.0020936*$u3;
      }


   if ($Y >= 1941 and $Y < 1961)
      {
       $u  = $y - 1950;
       $u2 = $u * $u;
       $u3 = $u * $u2;

       return 29.07 + 0.407*$u - $u2/233 + $u3/2547;
      }


   if ($Y >= 1961 and $Y < 1986)
      {
       $u = $y - 1975;
       $u2 = $u * $u;
       $u3 = $u * $u2;

       return 45.45 + 1.067*$u - $u2/260 - $u3/718;
      }


   if ($Y >= 1986 and $Y < 2005)
      {
       $u  = $y - 2000;
       $u2 = $u * $u;
       $u3 = $u * $u2;
       $u4 = $u * $u3;
       $u5 = $u * $u4;

       return 63.86 + 0.3345*$u
                    - 0.060374*$u2
                    + 0.0017275*$u3
                    + 0.000651814*$u4
                    + 0.00002373599*$u5;
      }


   if ($Y >= 2005 and $Y < 2050)
      {
       $u  = $y - 2000;

       return 62.92 + 0.32217*$u + 0.005589*$u*$u;
      }


   if ($Y >= 2050 and $Y <= 2150)
      {
       $u  = ($y - 1820) / 100;

       return 32*$u*$u - 0.5628*(2150 - $y) - 20;
      }


   if ($Y > 2150 and $Y <= 3000)
      {
       $u  = ($y - 1820) / 100;

       return 32*$u*$u - 20;
      }

   return FALSE;

} // End of  _Delta_T(...)







/*
  ========================================================================
  Computes the nutation in obliquity (delta epsilon) in
  degrees according to the IAU 2000B nutation series.

  This value is applied to the mean obliquity of the ecliptic to
  obtain the true, apparent obliquity.

  The series consists of 77 terms.

  ARGUMENT:
  $JD = JD number at the moment (TDB) for which the
        nutation in obliquity is required.

  ERRORS:
  No error checking.

  ========================================================================
*/


// THIS WILL BE NEW FUTURE VERSION TO REPLACE
// THE DEPRECATED dEps() FUNCTION VERSION.
// BOTH ARE FUNCTIONALLY IDENTICAL.
   function Delta_Eps ($JDTT)
  {
   return dEps ($JDTT);
  }

   FUNCTION dEps ($JDTT)
{
   $T = ($JDTT - 2451545) / 36525;

   $T2 = $T * $T;
   $T3 = $T * $T2;
   $T4 = $T * $T3;

// -------------------------------------------
// Compute mean anomaly of the Moon in radians

   $L  = deg2rad((485868.249036 + 1717915923.2178*$T + 31.8792*$T2
       + 0.051635*$T3 - 0.0002447*$T4) / 3600);


// ------------------------------------------
// Compute mean anomaly of the Sun in radians

   $Lp = deg2rad((1287104.79305 + 129596581.0481*$T
       - 0.5532*$T2  + 0.000136*$T3 - 0.00001149*$T4) / 3600);


// ------------------------------------------------------------
// Compute mean argument of the latitude of the Moon in radians

   $F  = deg2rad((335779.526232 + 1739527262.8478*$T
       - 12.7512*$T2 - 0.001037*$T3 + 0.00000417*$T4) / 3600);


// -----------------------------------------------------------
// Compute mean elongation of the Moon from the Sun in radians

   $D  = deg2rad((1072260.70369 + 1602961601.2090*$T
       - 6.3706*$T2  + 0.006593*$T3 - 0.00003169*$T4) / 3600);


// ------------------------------------------------------------------------
// Compute mean longitude of the mean ascending node of the Moon in radians

   $Om = deg2rad((450160.398036 - 6962890.5431*$T
       + 7.4722*$T2  + 0.007702*$T3 - 0.00005939*$T4) / 3600);

// --------------------------------------------------------------
// Sum series for nutation in obliquity (dEps) in arc sec * 10E+7

$s = 0;
$s += (92052331 + 9086*$T)*cos($Om) + 15377*sin($Om);
$s += (5730336 - 3015*$T)*cos(2*($F - $D + $Om)) - 4587*sin(2*($F - $D + $Om));
$s += (978459 - 485*$T)*cos(2*($F + $Om)) + 1374*sin(2*($F + $Om));
$s += (-897492 + 470*$T)*cos(2*$Om) - 291*sin(2*$Om);
$s += (73871 - 184*$T)*cos($Lp) - 1924*sin($Lp);
$s += (224386 - 677*$T)*cos($Lp + 2*($F - $D + $Om)) - 174*sin($Lp + 2*($F - $D + $Om));
$s -= 6750*cos($L) - 358*sin($L);
$s += (200728 + 18*$T)*cos(2*$F + $Om) + 318*sin(2*$F + $Om);
$s += (129025 - 63*$T)*cos($L + 2*($F + $Om)) + 367*sin($L + 2*($F + $Om));
$s += (-95929 + 299*$T)*cos(2*($F - $D + $Om) - $Lp) + 132*sin(2*($F - $D + $Om) - $Lp);
$s += (-68982 - 9*$T)*cos(2*($F - $D) + $Om) + 39*sin(2*($F - $D) + $Om);
$s += (-53311 + 32*$T)*cos(2*($F + $Om) - $L) - 4*sin(2*($F + $Om) - $L);
$s -= 1235*cos(2*$D - $L) - 82*sin(2*$D - $L);
$s -= 33228*cos($L + $Om) + 9*sin($L + $Om);
$s += 31429*cos($Om - $L) - 75*sin($Om - $L);
$s += (25543 - 11*$T)*cos(2*($F + $D + $Om) - $L) + 66*sin(2*($F + $D + $Om) - $L);
$s += 26366*cos($L + 2*$F + $Om) + 78*sin($L + 2*$F + $Om);
$s += (-24236 - 10*$T)*cos(2*($F - $L) + $Om) + 20*sin(2*($F - $L) + $Om);
$s -= 1220*cos(2*$D) - 29*sin(2*$D);
$s += (16452 - 11*$T)*cos(2*($F + $D + $Om)) + 68*sin(2*($F + $D + $Om));
$s -= 13870*cos(-2*($Lp - $F + $D - $Om));
$s += 477*cos(2*($D - $L)) - 25*sin(2*($D - $L));
$s += (13238 - 11*$T)*cos(2*($L + $F + $Om)) + 59*sin(2*($L + $F + $Om));
$s += (-12338 + 10*$T)*cos($L + 2*($F - $D + $Om)) - 3*sin($L + 2*($F - $D + $Om));
$s -= 10758*cos(2*$F + $Om - $L) + 3*sin(2*$F + $Om - $L);
$s -= 609*cos(2*$L) - 13*sin(2*$L);
$s -= 550*cos(2*$F) - 11*sin(2*$F);
$s += (8551 - 2*$T)*cos($Lp + $Om) - 45*sin($Lp + $Om);
$s -= 8001*cos(-$L + 2*$D + $Om) + sin(-$L + 2*$D + $Om);
$s += (6850 - 42*$T)*cos(2*($Lp + $F - $D + $Om)) - 5*sin(2*($Lp + $F - $D + $Om));
$s -= 167*cos(2*($D - $F)) - 13*sin(2*($D - $F));
$s += 6953*cos($L - 2*$D + $Om) - 14*sin($L - 2*$D + $Om);
$s += 6415*cos($Om - $Lp) + 26*sin($Om - $Lp);
$s += 5222*cos(2*($F + $D) + $Om - $L) + 15*sin(2*($F + $D) + $Om - $L);
$s += (168 - $T)*cos(2*$Lp) + 10*sin(2*$Lp);
$s += 3268*cos($L + 2*($F + $D + $Om)) + 19*sin($L + 2*($F + $D + $Om));
$s += 104*cos(2*($F - $L)) + 2*sin(2*($F - $L));
$s -= 3250*cos($Lp + 2*($F + $Om)) + 5*sin($Lp + 2*($F + $Om));
$s += 3353*cos(2*($F + $D) + $Om) + 14*sin(2*($F + $D) + $Om);
$s += 3070*cos(2*($F + $Om) - $Lp) + 4*sin(2*($F + $Om) - $Lp);
$s += 3272*cos(2*$D + $Om) + 4*sin(2*$D + $Om);
$s -= 3045*cos($L + 2*($F - $D) + $Om) + sin($L + 2*($F - $D) + $Om);
$s -= 2768*cos(2*($L + $F - $D + $Om)) + 4*sin(2*($L + $F - $D + $Om));
$s += 3041*cos(2*($D - $L) + $Om) - 5*sin(2*($D - $L) + $Om);
$s += 2695*cos(2*($L + $F) + $Om) + 12*sin(2*($L + $F) + $Om);
$s += 2719*cos(2*($F - $D) + $Om - $Lp) - 3*sin(2*($F - $D) + $Om - $Lp);
$s += 2720*cos($Om - 2*$D) - 9*sin($Om - 2*$D);
$s -= 51*cos(-$L - $Lp + 2*$D) - 4*sin(-$L - $Lp + 2*$D);
$s -= 2206*cos(2*($L - $D) + $Om) - sin(2*($L - $D) + $Om);
$s -= 199*cos($L + 2*$D) - 2*sin($L + 2*$D);
$s -= 1900*cos($Lp + 2*($F - $D) + $Om) - sin($Lp + 2*($F - $D) + $Om);
$s -= 41*cos($L - $Lp) - 3*sin($L - $Lp);
$s += 1313*cos(-2*($L - $F - $Om)) - sin(-2*($L - $F - $Om));
$s += 1233*cos(3*$L + 2*($F + $Om)) + 7*sin(3*$L + 2*($F + $Om));
$s -= 81*cos(-$Lp + 2*$D) - 2*sin(-$Lp + 2*$D);
$s += 1232*cos($L - $Lp + 2*($F + $Om)) + 4*sin($L - $Lp + 2*($F + $Om));
$s -= 20*cos($D) + 2*sin($D);
$s += 1207*cos(2*($F + $D + $Om) - $L - $Lp) + 3*sin(2*($F + $D + $Om) - $L - $Lp);
$s += 40*cos(2*$F - $L) - 2*sin(2*$F - $L);
$s += 1129*cos(-$Lp + 2*($F + $D + $Om)) + 5*sin(-$Lp + 2*($F + $D + $Om));
$s += 1266*cos($Om - 2*$L) - 4*sin($Om - 2*$L);
$s -= 1062*cos($L + $Lp + 2*($F + $Om)) + 3*sin($L + $Lp + 2*($F + $Om));
$s -= 1129*cos(2*$L + $Om) + 2*sin(2*$L + $Om);
$s -= 9*cos($Lp + $D - $L);
$s += 35*cos($L + $Lp) - 2*sin($L + $Lp);
$s -= 107*cos($L + 2*$F) - sin($L + 2*$F);
$s += 1073*cos(2*($F - $D) + $Om - $L) - 2*sin(2*($F - $D) + $Om - $L);
$s += 854*cos($L + 2*$Om);
$s -= 553*cos($D - $L) + 139*sin($D - $L);
$s -= 710*cos(2*($F + $Om) + $D) + 2*sin(2*($F + $Om) + $D);
$s += 647*cos(2*($F + 2*$D + $Om) - $L) + 4*sin(2*($F + 2*$D + $Om) - $L);
$s -= 700*cos($Lp + $D + $Om - $L);
$s += 672*cos(-2*($Lp - $F + $D) + $Om);
$s += 663*cos($L + 2*($F + $D) + $Om) + 4*sin($L + 2*($F + $D) + $Om);
$s -= 594*cos(-2*($L - $F - $D - $Om)) + 2*sin(-2*($L - $F - $D - $Om));
$s -= 610*cos(2*$Om - $L) - 2*sin(2*$Om - $L);
$s -= 556*cos($L + $Lp + 2*($F - $D + $Om));

// ----------------------------
// Return nutation in obliquity
// expressed in degrees.

   return $s / 36000000000;


} // End of  dEps(...)



/*

   ========================================================================
   Computes the nutation in ecliptical longitude (Delta psi)
   in degrees according to the IAU 2000B nutation series.

   This value is applied to the mean ecliptical longitude to obtain the
   true, apparent ecliptical longitude.

   The series consists of 77 terms.

   ARGUMENT:
   $JDTT = Astronomical JD number corresponding to date and time (TT).

   ERRORS:
   No error checking.
   ========================================================================
*/


   function Delta_Psi($JDTT)
  {
   return dPsi ($JDTT);
  }

   function dPsi ($JDTT)
{
// ---------------------------------------------
// Compute time (T) in Julian centuries reckoned
// from J2000.0 and 4 powers of T.

   $T  = ($JDTT - 2451545) / 36525;

   $T2 = $T * $T;
   $T3 = $T * $T2;
   $T4 = $T * $T3;

// -------------------------------------------
// Compute mean anomaly of the Moon in radians

   $L  = deg2rad((485868.249036 + 1717915923.2178*$T + 31.8792*$T2
       + 0.051635*$T3 - 0.0002447*$T4) / 3600);


// ------------------------------------------
// Compute mean anomaly of the Sun in radians

   $Lp = deg2rad((1287104.79305 + 129596581.0481*$T
       - 0.5532*$T2  + 0.000136*$T3 - 0.00001149*$T4) / 3600);


// ------------------------------------------------------------
// Compute mean argument of the latitude of the Moon in radians

   $F  = deg2rad((335779.526232 + 1739527262.8478*$T
       - 12.7512*$T2 - 0.001037*$T3 + 0.00000417*$T4) / 3600);


// -----------------------------------------------------------
// Compute mean elongation of the Moon from the Sun in radians

   $D  = deg2rad((1072260.70369 + 1602961601.2090*$T
       - 6.3706*$T2  + 0.006593*$T3 - 0.00003169*$T4) / 3600);


// ------------------------------------------------------------------------
// Compute mean longitude of the mean ascending node of the Moon in radians

   $Om = deg2rad((450160.398036 - 6962890.5431*$T
       + 7.4722*$T2  + 0.007702*$T3 - 0.00005939*$T4) / 3600);

// -----------------------------------------------------------------------
// Sum 2000B series for nutation in longitude (dPsi) in arc sec * 10000000

$s = 0;
$s += (-172064161 - 174666*$T)*sin($Om) + 33386*cos($Om);
$s += (-13170906 - 1675*$T)*sin(2*($F - $D + $Om)) - 13696*cos(2*($F - $D + $Om));
$s += (-2276413 - 234*$T)*sin(2*($F + $Om)) + 2796*cos(2*($F + $Om));
$s += (2074554 + 207*$T)*sin(2*$Om) - 698*cos(2*$Om);
$s += (1475877 - 3633*$T)*sin($Lp) + 11817*cos($Lp);
$s += (-516821 + 1226*$T)*sin($Lp + 2*($F - $D + $Om)) - 524*cos($Lp + 2*($F - $D + $Om));
$s += (711159 + 73*$T)*sin($L) - 872*cos($L);
$s += (-387298 - 367*$T)*sin(2*$F + $Om) + 380*cos(2*$F + $Om);
$s += (-301461 - 36*$T)*sin($L + 2*($F + $Om)) + 816*cos($L + 2*($F + $Om));
$s += (215829 - 494*$T)*sin(2*($F - $D + $Om) - $Lp) + 111*cos(2*($F - $D + $Om) - $Lp);
$s += (128227 + 137*$T)*sin(2*($F - $D) + $Om) + 181*cos(2*($F - $D) + $Om);
$s += (123457 + 11*$T)*sin(2*($F + $Om) - $L) + 19*cos(2*($F + $Om) - $L);
$s += (156994 + 10*$T)*sin(2*$D - $L) - 168*cos(2*$D - $L);
$s += (63110 + 63*$T)*sin($L + $Om) + 27*cos($L + $Om);
$s += (-57976 - 63*$T)*sin($Om - $L) - 189*cos($Om - $L);
$s += (-59641 - 11*$T)*sin(2*($F + $D + $Om) - $L) + 149*cos(2*($F + $D + $Om) - $L);
$s += (-51613 - 42*$T)*sin($L + 2*$F + $Om) + 129*cos($L + 2*$F + $Om);
$s += (45893 + 50*$T)*sin(2*($F - $L) + $Om) + 31*cos(2*($F - $L) + $Om);
$s += (63384 + 11*$T)*sin(2*$D) - 150*cos(2*$D);
$s += (-38571 - $T)*sin(2*($F + $D + $Om)) + 158*cos(2*($F + $D + $Om));
$s += 32481*sin(2*($F - $Lp - $D + $Om));
$s -= 47722*sin(2*($D - $L)) + 18*cos(2*($D - $L));
$s += (-31046 - $T)*sin(2*($L + $F + $Om)) + 131*cos(2*($L + $F + $Om));
$s += 28593*sin($L + 2*($F - $D + $Om)) - cos($L + 2*($F - $D + $Om));
$s += (20441 + 21*$T)*sin(2*$F - $L + $Om) + 10*cos(2*$F - $L + $Om);
$s += 29243*sin(2*$L) - 74*cos(2*$L);
$s += 25887*sin(2*$F) - 66*cos(2*$F);
$s += (-14053 - 25*$T)*sin($Lp + $Om) + 79*cos($Lp + $Om);
$s += (15164 + 10*$T)*sin(2*$D - $L + $Om) + 11*cos(2*$D - $L + $Om);
$s += (-15794 + 72*$T)*sin(2*($Lp + $F - $D + $Om)) - 16*cos(2*($Lp + $F - $D + $Om));
$s += 21783*sin(2*($D - $F)) + 13*cos(2*($D - $F));
$s += (-12873 - 10*$T)*sin($L - 2*$D + $Om) - 37*cos($L - 2*$D + $Om);
$s += (-12654 + 11*$T)*sin($Om - $Lp) + 63*cos($Om - $Lp);
$s -= 10204*sin(2*($F + $D) - $L + $Om) - 25*cos(2*($F + $D) - $L + $Om);
$s += (16707 - 85*$T)*sin(2*$Lp) - 10*cos(2*$Lp);
$s -= 7691*sin($L + 2*($F + $D + $Om)) - 44*cos($L + 2*($F + $D + $Om));
$s -= 11024*sin(-2*$L + 2*$F) + 14*cos(2*($F - $L));
$s += (7566 - 21*$T)*sin($Lp + 2*($F + $Om)) - 11*cos($Lp + 2*($F + $Om));
$s += (-6637 - 11*$T)*sin(2*($F + $D) + $Om) + 25*cos(2*($F + $D) + $Om);
$s += (-7141 + 21*$T)*sin(2*($F + $Om) - $Lp) + 8*cos(2*($F + $Om) - $Lp);
$s += (-6302 - 11*$T)*sin(2*$D + $Om) + 2*cos(2*$D + $Om);
$s += (5800 + 10*$T)*sin($L + 2*($F - $D) + $Om) + 2*cos($L + 2*($F - $D) + $Om);
$s += 6443*sin(2*($L + $F - $D + $Om)) - 7*cos(2*($L + $F - $D + $Om));
$s += (-5774 - 11*$T)*sin(2*($D - $L) + $Om) - 15*cos(2*($D - $L) + $Om);
$s -= 5350*sin(2*($L + $F) + $Om) - 21*cos(2*($L + $F) + $Om);
$s += (-4752 - 11*$T)*sin(2*($F - $D) - $Lp + $Om) - 3*cos(2*($F - $D) - $Lp + $Om);
$s += (-4940 - 11*$T)*sin($Om - 2*$D) - 21*cos($Om - 2*$D);
$s += 7350*sin(2*$D - $L - $Lp) - 8*cos(2*$D - $L - $Lp);
$s += 4065*sin(2*($L - $D) + $Om) + 6*cos(2*($L - $D) + $Om);
$s += 6579*sin($L + 2*$D) - 24*cos($L + 2*$D);
$s += 3579*sin($Lp + 2*($F - $D) + $Om) + 5*cos($Lp + 2*($F - $D) + $Om);
$s += 4725*sin($L - $Lp) - 6*cos($L - $Lp);
$s -= 3075*sin(2*($F - $L  + $Om)) + 2*cos(2*($F - $L  + $Om));
$s -= 2904*sin(3*$L + 2*($F + $Om)) - 15*cos(3*$L + 2*($F + $Om));
$s += 4348*sin(2*$D - $Lp) - 10*cos(2*$D - $Lp);
$s -= 2878*sin($L - $Lp + 2*($F + $Om)) - 8*cos($L - $Lp + 2*($F + $Om));
$s -= 4230*sin($D) - 5*cos($D);
$s -= 2819*sin(2*($F + $D + $Om) - $L - $Lp) - 7*cos(2*($F + $D + $Om) - $L - $Lp);
$s -= 4056*sin(2*$F - $L) - 5*cos(2*$F - $L);
$s -= 2647*sin(2*($F + $D + $Om) - $Lp) - 11*cos(2*($F + $D + $Om) - $Lp);
$s -= 2294*sin($Om - 2*$L) + 10*cos($Om - 2*$L);
$s += 2481*sin($L + $Lp + 2*($F + $Om)) - 7*cos($L + $Lp + 2*($F + $Om));
$s += 2179*sin(2*$L + $Om) - 2*cos(2*$L + $Om);
$s += 3276*sin($Lp - $L + $D) + cos($Lp - $L + $D);
$s -= 3389*sin($L + $Lp) - 5*cos($L + $Lp);
$s += 3339*sin($L + 2*$F) - 13*cos($L + 2*$F);
$s -= 1987*sin(2*($F - $D) - $L + $Om) + 6*cos(2*($F - $D) - $L + $Om);
$s -= 1981*sin($L + 2*$Om);
$s += 4026*sin($D - $L) - 353*cos($D - $L);
$s += 1660*sin($D + 2*($F + $Om)) - 5*cos($D + 2*($F + $Om));
$s -= 1521*sin(2*($F + 2*$D + $Om) - $L) - 9*cos(2*($F + 2*$D + $Om) - $L);
$s += 1314*sin($Lp - $L + $D + $Om);
$s -= 1283*sin(2*($F - $Lp - $D) + $Om);
$s -= 1331*sin($L + 2*($F + $D) + $Om) - 8*cos($L + 2*($F + $D) + $Om);
$s += 1383*sin(2*($F - $L + $D + $Om)) - 2*cos(2*($F - $L + $D + $Om));
$s += 1405*sin(2*$Om - $L) + 4*cos(2*$Om - $L);
$s += 1290*sin($L + $Lp + 2*($F - $D + $Om));

// ----------------------------------------
// Return nutation in longitude in degrees.

   return $s / 36000000000;

} // End of  dPsi ()



/*
   Computes the nutation in right ascension or the equation of the
   equinoxes, in degrees.

   This value is the difference (True Equinox) minus (Mean Equinox).

   The IAU 2000B nutation series is applied.

   LEVEL 0 DEPENDENCIES:
   Mean_Eps()
   dEps()
   dPsi()


   NOTE:
   Degrees * 240 = Seconds of RA
   ========================================================================
*/

   function Delta_Equinox($JDTT)
  {
   return dEquinox ($JDTT);
  }

   FUNCTION dEquinox ($JDTT)
{
// ---------------------------------------------
// COMPUTE COSINE OF TRUE OBLIQUITY OF ECLIPTIC.

   $CosEpsTrue = cosd(Mean_Eps($JDTT) + Delta_Eps($JDTT));

// --------------------------------------------
// RETURN EQUATION OF THE EQUINOXES IN DEGREES.

   return Delta_Psi($JDTT) * $CosEpsTrue;

} // End of  dEquinox()



/*
   This function computes the geocentric ecliptical coordinates,
   given the JDTT and geocentric equatorial coordinates.

   INPUT:
   JDTT    = JDTT corresponding to Date/Time/Zone/dT
   RAHrs   = Geocentric equatorial RA of body in hours
   DeclDeg = Geocentric equatorial declination of body in degrees

   OUTPUT:
   Lm = Geocentric ecliptical longitude of body in degrees
   Bm = Geocentric ecliptical latitude of body in degrees

*/
   function Equ_to_Ecl_Coords($JDTT, $RAHrs, $DeclDeg)
{
// READ ARGUMENT VALUES.
   $J = $JDTT;
   $a = $RAHrs * 15; // Compute RA in degrees.
   $d = $DeclDeg;

// COMPUTE THE TRUE OBLIQUITY OF THE ECLIPTIC,
// CORRECTED FOR NUTATION.
   $e = Mean_Eps($J) + Delta_Eps($J);

// COMPUTE GEOCENTRIC ECLIPTICAL LONGITUDE.
   $Lm = atand2(sind($a)*cosd($e) + tand($d)*sind($e), cosd($a));

// COMPUTE GEOCENTRIC ECLIPTICAL LATITUDE.
   $Bm = asind(sind($d)*cosd($e) - cosd($d)*sind($e)*sind($a));

// DONE:
   return "$Lm $Bm";

} // END OF  Equ_to_Ecl_Coords(...)






/*
  ========================================================================
  Computes the mean obliquity of the ecliptic in degrees for the current
  JDTT argument corresponding to any given date/time.

  This ecliptic obliquity function is based on a formula derived by
  J. Laskar, Astronomy and Astrophysics, 1968, Vol. 157, p68.

  The estimated accuracy of this formula is ±0.01 arc second between
  the years 1000 AD and 3000 AD and a few arc seconds after 10,000
  years.  It is only valid in the range ±10,000 years either way
  of J2000.0

  Over the long term, it is more accurate than the formula adopted by
  the International Astronomical Union (IAU).


  ---------
  ARGUMENT:
  $JDTT = JD number at the moment (TDB) for which the mean obliquity
          of the ecliptic is required.
  ========================================================================
*/

   function Mean_Eps ($JDTT)
{

// ----------------------------------------------
// Compute the time in Julian centuries reckoned
// from J2000.0 corresponding to the JD argument
// and then divide by 100 for decamillennia.

   $t = ($JDTT - 2451545) / 36525 / 100;

// ------------------------------------------
// Compute mean obliquity (e) of the ecliptic
// expressed in arc seconds.

   $p = $t*$t;

   $e = 84381.448 - 4680.93*$t;

   $e -=    1.55*$p;  $p *= $t;
   $e += 1999.25*$p;  $p *= $t;
   $e -=   51.38*$p;  $p *= $t;
   $e -=  249.67*$p;  $p *= $t;
   $e -=   39.05*$p;  $p *= $t;
   $e +=    7.12*$p;  $p *= $t;
   $e +=   27.87*$p;  $p *= $t;
   $e +=    5.79*$p;  $p *= $t;
   $e +=    2.45*$p;

// -----------------------------------
// Done.  Return obliquity in degrees.
   return $e / 3600;

} // End of  Mean_Eps (...)






/*
   ===================================================================
   Computes the local or Greenwich mean sidereal time in degrees
   for any given JD and geographic longitude arguments.

   ARGUMENTS:
   JDTT    = JD argument for any given date and time (TT)

   LonDeg  = Geographic longitude. (Negative = West)
             Default longitude = 0 degrees (= Greenwich)

   ERRORS:
   FALSE is returned if either argument is invalid.
   ===================================================================
*/

   function Mean_Sid_Time ($JDTT, $LonDeg=0)
{
// ------------------------------
// Check for invalid argument(s).
   if ($JDTT < 2305447 or $JDTT > 2524959 or !is_numeric($LonDeg)) {return FALSE;}

// ------------------------------------------------
// Compute time (t) in Julian millennia as reckoned
// from J2000.0 and corresponding to the JD number.
   $t = ($JDTT - 2451545) / 36525;

// ------------------------------------------------
// Compute Greenwich mean sidereal time in degrees.
   $GSTMeanDeg = 280.46061837 + 360.98564736629*($JDTT - 2451545)
               + ($t*$t * 0.000387993) - ($t*$t*$t / 38710000);

// -----------------------------------
// Compute local mean sidereal time at
// the given geographic longitude.
   $LSTMeanDeg = $GSTMeanDeg + $LonDeg;

// ----------------------------------------------------------------
// Modulate sidereal time angle to fall between 0 and +360 degrees.
   $w = abs($LSTMeanDeg);
   $w = (($LSTMeanDeg < 0)? -1:1)*($w - 360*floor($w/360));
   $w += ($w < 0)? 360:0;
   $LSTMeanDeg = $w;
   $LSTMeanHrs = $LSTMeanDeg / 15;

// -------------------------------------------
// Return mean sidereal time angle in degrees.
   return rtrim(rtrim($LSTMeanDeg,'0'), '.');

}  // End of  Mean_Sid_Time (...)



















// ************************
// ************************
// ANGLE HANDLING FUNCTIONS


/*
   =========================================================================
   This function returns the surface distance between two points on a sphere
   of any given radius, based on their latitude and longitude coordinates.
   This refers to an ideal sphere, but it can also be applied to the Earth
   or moon without any severe error.

   The radius can be given in any convenient units, such as kilometers or
   miles. Whatever units are used for the radius, also apply to the
   returned surface distance.

   ARGUMENTS:
   radius = Radius of the sphere in any convenient units

   Lat1, Lon1 = Latitude and longitude coordinates of 1st point
   Lat2, Lon2 = Latitude and longitude coordinates of 2nd point

*/

   function Dist_Between_Coords ($radius, $Lat1,$Lon1, $Lat2,$Lon2)
{
// Convert degree arguments to radians
   $LatA = deg2rad($Lat1);  $LonA = deg2rad($Lon1);
   $LatB = deg2rad($Lat2);  $LonB = deg2rad($Lon2);

// Compute differences between coordnates.
   $dLat = $LatB - $LatA;
   $dLon = $LonB - $LonA;

// Calculate the angle between the points.
   $w = sin($dLat/2)*sin($dLat/2) + cos($LatA)*cos($LatB)*sin($dLon/2)*sin($dLon/2);
   $a = 2*asin(sqrt($w));

// Compute and return distance in
// the same units as the radius.
   return $radius * $a;
}



// ---------------------------------------------------------------
// This function computes the geodesic distance between two points
// on a sphere from their longitude and latitude coordinates.
// The distance can be returned in kilometers (default) or miles.

   function Geodesic_Dist ($lon1, $lat1, $lon2, $lat2, $KmMi='km')
{
   $KmMiFactor = (strtolower($KmMi) == 'km')? 1:1.609344;

   $ff = 1 / 298.2572221; // Flattening

   $f  = ($lat1 + $lat2) / 2;
   $g  = ($lat1 - $lat2) / 2;
   $l  = ($lon1 - $lon2) / 2;

   $sf = sin($f*M_PI/180);
   $sg = sin($g*M_PI/180);
   $sl = sin($l*M_PI/180);
   $cf = cos($f*M_PI/180);
   $cg = cos($g*M_PI/180);
   $cl = cos($l*M_PI/180);

   $w1 = ($sg * $cl)*($sg * $cl);
   $w2 = ($cf * $sl)*($cf * $sl);
   $s  = $w1 + $w2;

   $w1 = ($cg * $cl)*($cg * $cl);
   $w2 = ($sf * $sl)*($sf * $sl);
   $c  = $w1 + $w2;
   $o  = atan(sqrt($s/$c));
   $r  = sqrt($s*$c) / $o;
   $d  = 2 * $o * 6378.137;

   $h1 = (3*$r-1) / (2*$c);
   $h2 = (3*$r+1) / (2*$s);

   $w1 = ($sf * $cg) * ($sf * $cg) * $h1 * $ff + 1;
   $w2 = ($cf * $sg) * ($cf * $sg) * $h2 * $ff;

   return $d * ($w1 - $w2) * $KmMiFactor;
}




/*
   This function computes the angular diameter of a
   sphere viewed at any distance from its center.

   The equation used accounts for the 3D perspective
   of the curved surface of the sphere vs distance.

   Radius and distance must be measured in same units.

   Returned angle is in degrees.
*/

   function Ang_Diam_at_Dist ($radius, $distance)
  {
   $R = floatval($radius);
   $D = floatval($distance);

// Return FALSE on error, if R*D <= zero.
   if ($R*$D <= 0) {return FALSE;}

// Return angular diameter of sphere, in degrees.
   return rad2deg(2*acos(SqRt($D*$D - $R*$R) / $D));
  }



/*
   ========================================================================
   This function modulates a negative or positive angle to fall within the
   range from 0 to +360 degrees to help handle angles that fall outside
   the standard circular values.  For example, -5 degrees would be
   converted to +355 degrees or 417 degrees would be converted
   to 57 degrees, etc.

   This function uses arbitrary precision arithmetic.


   ARGUMENTS:
   $DegStr = Degrees argument as a numerical string.
   ========================================================================
*/

   function Deg_Mod_360 ($DegStr)
{
   $SignVal = ($DegStr < 0)? -1:1;

   $a = bcmul($SignVal, "$DegStr", 20);

   $b = bcmul($SignVal, bcsub($a, bcmul(360, bcdiv($a, 360)), 20), 20);

   return rtrim(rtrim(bcadd($b, (($b < 0)? 360:0), 16), "0"), ".");

} // End of  Deg_Mod_360()






/*
   Computes the local azimuth (direction) and altitude
   relative to the local horizon, in degrees.

   JDTT    = Astronomical JD argument corresponding to date/time
   RAHrs   = Right Ascension of body, in HOURS - not degrees.
   DeclDeg = Declination of body, from 0 to ±90 degrees.

   LongitudeDeg = Geographic longitude (0 to ±180 degrees,  Neg/Pos = W/E)
   LatitudeDeg  = Geographic latitude  (0 to ±90  degrees,  Neg/Pos = S/N)

   Output  = Horizon coordinates as string = "AzimDeg DeclDeg"
   AzimDeg = Azimuth = Clockwise direction from North = 0  (0 to 360°)
   AltDeg  = Altitude relative to horizon (Neg/Pos = Below/Above horizon).
*/

   function Azim_Alt ($JDTT, $RAHrs, $DeclDeg, $LongitudeDeg=0, $LatitudeDeg=0)
{
   $JDTT    = floatval($JDTT);
   $RAHrs   = floatval($RAHrs);
   $DeclDeg = floatval($DeclDeg);
   $LonDeg  = floatval($LongitudeDeg);
   $LatDeg  = floatval($LatitudeDeg);

// Compute local true sidereal time.
   $LSTTrueDeg = Mean_Sid_Time($JDTT, $LonDeg) + dEquinox($JDTT);

// Compute local hour angle of body.
   $H = $LSTTrueDeg - $RAHrs*15;

// Compute azimuth clockwise from 0 to 360 degrees (North = 0 degrees).
   $AzimDeg = 180 + atand2(sind($H), (cosd($H)*sind($LatDeg) - tand($DeclDeg)*cosd($LatDeg)));
   $AzimDeg -= (($AzimDeg >= 360)? 360:0);

// Compute altitude relative to local horizon.  Negative = Below horizon.
   $SinAlt = sind($LatDeg)*sind($DeclDeg) + cosd($LatDeg)*cosd($DeclDeg)*cosd($H);
   $AltDeg = asind($SinAlt);

   return "$AzimDeg $AltDeg";

} // End of  Azim_Alt (...)








/*
   Convert DMS string to decimal degrees.

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

*/

   function DMS_to_Deg ($DMSstr)
{

// ----------------------------------------------------------
// Read angle/time string argument and force to lower case in
// the event any (deg d h m s : ° ' ") symbols are used.

   $dms = strtolower(trim($DMSstr));

// ------------------------------------------------------
// Convert any (deg d m s ° ' ") symbols into spaces.

   $dms = str_replace('deg', ' ', $dms);
   $dms = str_replace('d',   ' ', $dms);
   $dms = str_replace('m',   ' ', $dms);
   $dms = str_replace('s',   ' ', $dms);
   $dms = str_replace('°',   ' ', $dms);
   $dms = str_replace("'",   ' ', $dms);
   $dms = str_replace('"',   ' ', $dms);
   $dms = str_replace("&deg;",   ' ', $dms);
// ----------------------------------------------------
// Normalize the spacing and then split and extract the
// individual angle/time string elements (dh or hh, mm,ss).

   $dms = preg_replace("/\s+/", " ", trim($dms));
   $wdms = preg_split("[ ]", $dms);
   $wdmsCount = count($wdms);
   $dd = ($wdmsCount >= 1)? bcadd($wdms[0],"0", 20) : "0";
   $mm = ($wdmsCount >= 2)? bcadd($wdms[1],"0", 20) : "0";
   $ss = ($wdmsCount >= 3)? bcadd($wdms[2],"0", 20) : "0";

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

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

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

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

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

   $w2 = bcadd(bcadd(bcmul($dd,"3600",20),bcmul($mm,"60",20),20),$ss,20);

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

   if (bccomp($w2, "0", 20) == 0) {$NumSign = '';}

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

   return $NumSign . bcadd(bcdiv($w2,"3600",20), "0.00000000000000005",16);

} // End of  DMS_to_Deg (...)




/*
   ========================================================================
   Convert decimal degrees to degrees, minutes, seconds of arc.
   The angular elements are returned in a space-delimited string.

   Optional symbols are attached to the angle elements. The seconds can
   optionally be rounded to up to 3 decimals with 0=Default.

   Symbols mode = TRUE|FALSE or d|D
   TRUE|d|D means to apply DMS symbols to returned string.
   FALSE means no DMS symbols are applied to returned string.

   ========================================================================
*/

   function Deg_to_DMS ($AngDeg, $ssDecimals=0, $PosSignSymbol='', $SymbolsMode='d')
{

// Init
   $_d_ = $_m_ = $_s_ = '';

// ------------------------------------------------------
// Compute signed angular elements from decimal argument.
// This value is taken as decimal degrees.
   $sign = ($AngDeg < 0)? '-' : '';

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

   $degrees = abs($AngDeg);
   $dd      = floor($degrees);
   $minutes = 60*($degrees - $dd);
   $mm      = floor($minutes);
   $seconds = 60*($minutes - $mm);
   $ss      = sprintf("%1.3f", $seconds);

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

// ----------------------------
// Format the angular elements.
   $dd = sprintf("%02d",  $dd);
   $mm = sprintf("%02d",  $mm);
   $ss = sprintf("%1.$ssDecimals" . "f", $ss);

   if ($ss < 10) {$ss = "0$ss";}

// Attach optional DMS symbols.
// Default = TRUE. Can also use 'd' for DMS symbols flag.
   if ($SymbolsMode === TRUE or substr(strtolower($SymbolsMode),0,1) == 'd')
      {
       $_d_ = "&deg;";
       $_m_ = "'";
       $_s_ = '"';
      }

// Done.
   return "$sign$dd$_d_ $mm$_m_ $ss$_s_";

} // End of  Deg_to_DMS (...)





/*
   Converts decimal hours into an hours, minutes
   and seconds string.

   For example, the arguments (10.123456789, 3)
   hours would return the string "10h 07m 24.444s"

   $PosSignSymbol = + if positive value or empty = No sign symbol

   $SymbolsMode = h or :
                  FALSE = No symbols
                  : = Use colons (:) instead of h,m,s symbols
                  h = Use h,m,s symbols instead of colons (= Default).

*/
   function Hours_to_HMS ($hours, $ssDecimals=0, $PosSignSymbol='', $SymbolsMode='h')
{

// ---------------------------
// Initialize symbol carriers.
   $_h_ = $_m_ = $_s_ = '';

   if (trim($PosSignSymbol) == '')  {$PosSignSymbol = FALSE;}

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

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

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

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

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

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

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

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

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

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

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

// Done.
   return "$sign$hh$_h_$mm$_m_$ss$_s_";

} // End of  Hours_to_HMS(...)


   function HMS_to_Hours ($hhmmss)
{

// -----------------------------------------------------------
// Read hour angle string argument and force to lower case in
// the event any (h m s :) symbols are used.

   $hms = strtolower(trim($hhmmss));

// ------------------------------------------------------
// Convert any (h m s :) symbols into spaces.

   $hms = str_replace('h', ' ', $hms);
   $hms = str_replace('m', ' ', $hms);
   $hms = str_replace('s', ' ', $hms);
   $hms = str_replace(':', ' ', $hms);


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

   $hms = preg_replace("/\s+/", " ", trim($hms));
   $whms = preg_split("[ ]", $hms);
   $whmsCount = count($whms);
   $hh = ($whmsCount >= 1)? bcadd($whms[0],"0", 20) : "0";
   $mm = ($whmsCount >= 2)? bcadd($whms[1],"0", 20) : "0";
   $ss = ($whmsCount >= 3)? bcadd($whms[2],"0", 20) : "0";

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

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

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

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

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

   $w2 = bcadd(bcadd(bcmul($hh,'3600',20),bcmul($mm,'60',20),20),$ss,20);

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

   if (bccomp($w2, '0', 20) == 0) {$NumSign = '';}

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

   $w =  bcadd(bcdiv($w2,3600, 20), '0.00000000000000005', 16);

// Trim off any redundant zeros/decimal point.
   return $NumSign . rtrim(rtrim($w, '0'), '.');

} // End of  HMS_to_Hours(...)





/*
   This function rounds off a given time string in decimal
   hours or 'hh:mm:ss.sss' format to the nearest hour,
   minute or second.

   This is primarily for those special cases when rounding
   event times to the nearest hour or minute is sufficient,
   rather than taking it out to a fraction of a second.

   Returns  hh,  hh:mm  or  hh:mm:ss

   FALSE is returned if $hmsMode is invalid.

*/

   function Round_HMS ($hhmmss, $HMSMode='hms')
{
   $HMSMode = strtolower($HMSMode);

// Convert to decimal hours.
   $hours = HMS_to_Hours($hhmmss);

   $nSign = substr($hours, 0,1);
   if ($nSign <> '-' and $nSign <> '+') {$nSign = '';}

   $hms = Hours_to_HMS(abs($hours), 3, '', '');

   list($hh, $mm, $ss) = preg_split("[ ]", $hms);

   $hh = floatval($hh);
   $mm = floatval($mm);
   $ss = floatval($ss);

// Convert minutes+seconds to decimal minutes.
   $minutes = $mm + $ss/60;

// Round to nearest hour.
   if ($HMSMode == 'h')
   {
    return $nSign.round($hh + $minutes/60, 0);
   }

// Round to nearest minute.
   if ($HMSMode == 'hm')
   {
    $mm = round($minutes, 0);
    if($mm == 60) {$hh++; $mm=0;}
    $hh = sprintf("%02d", $hh);
    $mm = sprintf("%02d", $mm);
    return "$nSign$hh:$mm";
   }

// Round to nearest second.
   if ($HMSMode == 'hms')
   {
    $ss = round($ss, 0);
    if($ss == 60) {$mm++; $ss=0;}
    if($mm == 60) {$hh++; $mm=0;}

    $hh = sprintf("%02d", $hh);
    $mm = sprintf("%02d", $mm);
    $ss = sprintf("%02d", $ss);
    return "$nSign$hh:$mm:$ss";
   }
    return FALSE;

} // End of  Round_HMS (...)




function DTZ_to_JD ($DateTimeZoneString)
{
// Set default Time/Zone strings
   $hhmmss = '00:00:00';
   $TZhhmm = '+00:00';

// Parse Date/Time/Zone argument into separate elements.
// Default values are equated to zero.  There MUST BE at
// least one argument (the date).  The Time and Zone are
// optional and default to zero values.
   $DTZStr = strip_wspace($DateTimeZoneString);
   $DTZ = preg_split("[ ]", $DTZStr);

// Count DTZ elements.
   $DTZCount = count($DTZ);

// Parse Date/Time/Zone elements.
   if ($DTZCount >= 1){$yyyymndd = $DTZ[0];}
   if ($DTZCount >= 2){  $hhmmss = $DTZ[1];}
   if ($DTZCount >= 3){  $TZhhmm = $DTZ[2];}

// Parse integer-encoded date element (year, month, day).
   $yyyy = intval(substr($yyyymndd, 0,4));
     $mn = substr($yyyymndd, 4,2); $m = intval($mn);
     $dd = substr($yyyymndd, 6,2); $d = intval($dd);

// Compute JD number corresponding to date elements.
   $JDNum = GregorianToJD($m, $d, $yyyy);

// Parse time elements and compute equivalent fraction of day.
   $TimeFracDay = HMS_to_Hours($hhmmss) / 24;

// Parse time zone elements and compute equivalent fraction of day.
   $TZFracDay = HMS_to_Hours($TZhhmm) / 24;

// Compute final JD (Julian Date) value from the JD number
// and fractional values for the time of day and time zone,
// rounded to 8-decimals.
   $JDate = sprintf("%7.8f", $JDNum - 0.5 + $TimeFracDay - $TZFracDay);

// Done.
   return $JDate;

} // End of  DTZ_to_JD (...)






   function DTZ_to_JDTT ($DateTimeZoneString)
  {
   return Date_Time_Zone_to_JDTT ($DateTimeZoneString);
  }



function Date_Time_Zone_to_JDTT ($DateTimeZoneString)
{
// Set default Time/Zone strings
   $hhmmss = '00:00:00';
   $TZhhmm = '+00:00';

// Parse Date/Time/Zone argument into separate elements.
// Default values are equated to zero.  There MUST BE at
// least one argument (the date).  The Time and Zone are
// optional and default to zero values.
   $DTZStr = strip_wspace($DateTimeZoneString);
   $DTZ = preg_split("[ ]", $DTZStr);

// Count DTZ elements.
   $DTZCount = count($DTZ);

// Parse Date/Time/Zone elements.
   if ($DTZCount >= 1){$yyyymndd = $DTZ[0];}
   if ($DTZCount >= 2){  $hhmmss = $DTZ[1];}
   if ($DTZCount >= 3){  $TZhhmm = $DTZ[2];}

// Parse integer-encoded date element (year, month, day).
   $yyyy = intval(substr($yyyymndd, 0,4));
     $mn = substr($yyyymndd, 4,2); $m = intval($mn);
     $dd = substr($yyyymndd, 6,2); $d = intval($dd);

// Compute JD number corresponding to date elements.
   $JDNum = GregorianToJD($m, $d, $yyyy);

// Parse time elements and compute equivalent fraction of day.
   $TimeFracDay = HMS_to_Hours($hhmmss) / 24;

// Parse time zone elements and compute equivalent fraction of day.
   $TZFracDay = HMS_to_Hours($TZhhmm) / 24;

// Compute Delta T resolved to nearest second.
   $dTFracDay = Delta_T($JDNum)/S_PER_DAY;

// Compute final JD (Julian Date) TT value rounded to 8-decimals.
   $JDTT = sprintf("%7.8f", $JDNum - 0.5 + $TimeFracDay - $TZFracDay + $dTFracDay);

// Done.
   return $JDTT;

} // End of  DTZ_to_JDTT (...)





   function JD_To_Date_Time_String ($JD)
{
// Read general working JD value argument.
   $wJD = floatval($JD);

// Compute corresponding general calendrical JD.
   $CJD = $wJD + 0.5;

// Compute calendrical JD number for date.
   $JDNum = floor($CJD);

// Compute the calendar date elements (mn, dn, Y).
   list($mn, $dn, $Y) = preg_split("[\/]", JDtoGregorian($JDNum));

// Get 3-letter day of week and month
// name abbreviation strings.
   $DoWStr = JDDayOfWeek($JDNum, 2);
   $MonthNameStr = JDMonthName($JDNum, 0);

// Compute time of day as fraction
// of day since beginning of date.
   $TimeFrac = $CJD - $JDNum;

// Compute time elements from the decimal
// value, resolved to nearest second.
   $dd      = 0;
   $hours   = 24*$TimeFrac;
   $hh      = floor($hours);
   $minutes = 60*($hours - $hh);
   $mm      = floor($minutes);
   $seconds = 60*($minutes - $mm);
   $ss      = round($seconds, 0);

// Account for 60s glitch.
   if ($ss == 60){$ss=0;  $mm++;}
   if ($mm == 60){$mm=0;  $hh++;}
   if ($hh == 24){$hh=0;  $dd++;}

// Format time elements into standard
// time string format (hh:mm:ss).
   $hh = sprintf("%02d", $hh);
   $mm = sprintf("%02d", $mm);
   $ss = sprintf("%02d", $ss);
   $hhmmss = "$hh:$mm:$ss";

   return "$Y $MonthNameStr " . sprintf("%02d", $dn) . " $DoWStr $hhmmss";

} // End of  JD_To_Date_Time_String(...)




/*
   ==========================================================
   This is the inverse Julian date function.
   Given a Julian Date, Time Zone Offset and date mode,
   it returns the date and time in either of two modes.

   The date can be returned in 2 modes.
   0 = Integer-encoded date = 19490520 19:21:27
   1 = Alpha date string = "1949 Oct 31 19:21:27 Mon"
*/
   function JD_Z_to_DT ($JDate, $TZhhmm='+00:00', $DateMode=1)
{
// Parse time zone elements and compute
// equivalent fraction of day.
   $TZHM = preg_split("[\:]", trim($TZhhmm));
   $TZHMCount = count($TZHM);
   $hh = $mm = $ss = 0;

   if ($TZHMCount >= 1)
      {
       $hh = $TZHM[0];
       $NegPos = (substr($hh,0,1) == '-')? -1:1;
       $hh = abs(floatval($TZHM[0]));
      }
   if ($TZHMCount >= 2){$mm = floatval($TZHM[1]);}
   if ($TZHMCount >= 3){$ss = floatval($TZHM[2]);}
   $TZFrac = $NegPos * (3600*$hh + 60*$mm) / 86400;

// Compute JD for given time zone.
   $JD = $JDate + $TZFrac;

// Compute JD number.
   $JDNum = floor($JD + 0.5);

// Compute time fraction in hours.
   $hours = 24*($JD - $JDNum + 0.5);

// Compute time elements from time fraction.
   $hh = floor($hours);
   $minutes = 60*($hours - $hh);
   $mm = floor($minutes);
   $seconds = 60*($minutes - $mm);
   $ss = round($seconds, 0);

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

// Format time elements.
   $hh = sprintf("%02d", $hh);
   $mm = sprintf("%02d", $mm);
   $ss = sprintf("%02d", $ss);
   $hhmmss = "$hh:$mm:$ss";

// Compute date elements (m, d, yyyy) corresponding
// to the JD number value.
   list($m, $d, $yyyy) = preg_split("[\/]", JDtoGregorian($JDNum));

// Construct Date/Time string for given time zone.
   $yyyymndd = 10000*$yyyy + 100*$m + $d;

// Construct date string according to DateMode  (1 = Default | 2)
// Mode 1 returns the date in the default integer-encoded Date/Time
// string format.
// EXAMPLE: "19491031 19:21:27"
   if ($DateMode == 1) {return "$yyyymndd $hhmmss";}

// Mode 2 returns the date in alpha numeric Date/Time/DoW string format.
// EXAMPLE: "1949 Oct 31 19:21:27 Mon"
   if ($DateMode == 2)
{
   $Mmm    = substr("JanFebMarAprMayJunJulAugSepOctNovDec", 3*($m-1), 3);
   $dd     = sprintf("%02d", $d);
   $DoWStr = substr("SunMonTueWedThuFriSat", 3*(($JDNum + 1) % 7), 3);

   return "$yyyy $Mmm $dd $DoWStr $hhmmss";
}
   return FALSE; // ERROR = Unknown DateMode

} // End of  JDZ_to_DT (...)




/*
   This function finds the Nth DoW
*/

   function Nth_DoW_of_mY ($n, $DoW, $m, $y)
{
   $JDNum1 = GregorianToJD ($m, 1, $y);
   $DoW1   = ($JDNum1 + 1) % 7;
   $dDoW   = $DoW - $DoW1;
   $JDNumX = $JDNum1 + (($dDoW < 0)? $dDoW + 7 : $dDoW) + 7*($n-1);

   list ($m,$d,$y) = preg_split("[\/]", JDtoGregorian($JDNumX));

   $yyyymndn = sprintf('%04d', $y).sprintf('%02d', $m).sprintf('%02d', $d);

   return $yyyymndn; // Return integer-encoded date.

} // End of  Nth_DoW_of_Ym(...)


/*
   Thisa function finds the last (DoW) of any month of any
   given year, where DoW refers to the day of the week.

   mn = Month number (1 to 12
   DoW values:
   0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5-Fri and 6=Sat

*/

   function Last_DoW_of_mY ($DoW, $mn, $yyyy)
{
// --------------------------
// READ MONTH/YEAR ARGUMENTS.

   $m = intval($mn);
   $y = intval($yyyy);

// ---------------------------------------------
// FIND JD NUMBER OF 1ST DoW OF FOLLOWING MONTH.
// IF DECEMBER, WE FIND THE JD NUMBER OF THE 1ST
// DOW OF JANUARY OF THE FOLLOWING YEAR AND THEN
// SUBTRACT 7.

   $m++;

   if ($m == 12) {$m = 1;   $y++;}

   $JDNum1 = GregorianToJD ($m, 1, $y);
   $DoW1   = ($JDNum1 + 1) % 7;
   $dDoW   = $DoW - $DoW1;

// ---------------------------------------------
// COMPUTE JD NUMBER OF LAST DOW OF GIVEN MONTH.

   $JDNumX = $JDNum1 + (($dDoW < 0)? $dDoW + 7 : $dDoW) - 7;

// ------------------------------------------------
// CONSTRUCT AND RETURN INTEGER-ENCODED DATE VALUE.

   list
  (
   $m,
   $d,
   $y
  ) = preg_split("[\/]", JDtoGregorian($JDNumX));

   return sprintf('%04d', $y).sprintf('%02d', $m).sprintf('%02d', $d);

} // End of  Last_DoW_of_Ym(...)





/*
   THIS FUNCTION RETURN A GREGORIAN DATE STRING CORRESPONDING
   TO ANY GIVEN JULIAN DATE OR JD NUMBER ARGUMENT.

   EXAMPLE:
   2433057 RETURNS THE STRING '1949 May 20 Fri'

   JD NUMBER OR FULL JULIAN DATE (JD) CAN BE USED.
   ONLY THE DATE IS RETURNED.
*/
   function JD_to_Y_Mmm_dn_DoW ($JD)
{
   $JDNum = floor($JD + 0.5);

   list
  (
   $m,
   $d,
   $y
  ) = preg_split("[\/]",JDtoGregorian($JDNum));

   $yyyy = sprintf('%04d', $y);
   $mn   = sprintf('%02d', $m);
   $dn   = sprintf('%02d', $d);

   $MmmStr = substr('JanFebMarAprMayJunJulAugSepOctNovDec', 3*($m-1), 3);
   $DoWStr = substr('SunMonTueWedThuFriSat', 3*(($JDNum + 1) % 7), 3);

   return "$yyyy $MmmStr $dn $DoWStr";

} // End of  JD_Num_to_Y_Mmm_dn(...)




/*
   This function translates an assumed time string into standard form.
   Applies ONLY to purely numerical time strings.


   Time String          Translates to
   1                    01:00:00
   12                   12:00:00
   123                  01:23:00
   1234                 12:34:00
   12345                01:23:45
   123456               12:34:56

Numeric strings are read in 2-digit groups from R to L

Odd length numbers are converted to even length and then
the even length numbers are converted to 6 digit values.

THIS FUMCTION DOES NOT CHECK FOR INVALID TIMES.  IT ONLY
CASTS THE GIVEN NUMERIC TIME STRING IN NORMAL HH:MM:SS
FORMAT AS DEFINED ABOVE.

*/

   function Num_HMS_Trans ($HHMMSS)
{
   $whhmmss = trim($HHMMSS);

// Return default 00:00:00 if empty argument.
   if ($whhmmss == '') {return '00:00:00';}

// If any spaces or colons, attempt to translate as given.
   if (strpos($whhmmss, ':') !== FALSE or strpos($whhmmss, ' ') !== FALSE)
  {
   $hours = HMS_to_Hours($whhmmss);

   while($hours >= 24) {$hours -= 24;}

   return Hours_to_HMS($hours, 0, '', ':');
  }

// Truncate to first six digit characters only.
// Anything beyond the 6th digit will be cut off.
   $whhmmss = substr($whhmmss, 0,6);

// Try to break integer-encoded HHMMSS into 3 groups of 2 digits
// from R to L.  Unspecified elements default to zero strings.
   $n = strlen($whhmmss);

   if ($n == 1 or $n == 3 or $n == 5)
      {$whhmmss = "0$whhmmss";  $n = strlen($whhmmss);}

   for ($i=0;  $i <= $n/2;   $i++) {$whhmmss .= '00';}

// Construct normal standard time string with colons
// between 2-digit groups.
   $hhmmss = '';
   for($i=0;   $i < 3;   $i++)  {$hhmmss .= substr($whhmmss, 2*$i, 2).':';}

// Trim off any redundant ':' character from R end.
// Done.
   return RTrim($hhmmss, ':');

} // End of  Num_HMS_Trans(...)





/*
   CUSTOM FUNCTION SIMILAR TO PHP NATIVE CheckDate() FUNCTION,
   BUT USING A SINGLE INTEGER-ENCODED DATE ARGUMENT VALUE.

   THIS CUSTOMIZED FUNCTION ONLY TESTS FOR DATES INSIDE THE
   600-YEAR SPAN OF THE NASA/DE405 EPHEMERIS MODEL.  ANY
   DATES OUTSIDE THIS RANGE ARE ALSO CONSIDERED INVALID.

   THIS FUNCTION TESTS THE INTEGER-ENCODED DATE FOR VALIDITY
   AND RETURNS A BOOLEAN STATE VALUE (TRUE/FALSE).
   TRUE = OK
   FALSE = INVALID DATE

*/

   function Check_Date($yyyymmdd)
{

// READ INTEGER-ENCODED DATE ARGUMENT
// AND PARSE THE DATE ELEMENT VALUES.

   $w = intval($yyyymmdd);
   $Y = intval(substr($w,0,4));
   $m = intval(substr($w,4,2));
   $d = intval(substr($w,6,2));

// CHECK DATE FOR VALIDITY AND RETURNS TRUE/FALSE.
// TRUE  = OK
// FALSE = INVALID DATE

   return CheckDate($m, $d, $Y);

} // END OF  Check_Date(yyyymndn)






/*
   THIS FUNCTION TESTS A GIVEN hhmmss TIME STRING FOR VALIDITY
   AND RETURNS A BOOLEAN STATE VALUE (TRUE/FALSE).
   TRUE  = OK
   FALSE = INVALID TIME

   FOR A TIME TO BE VALID, THE SUM OF THE
   ELEMENTS MUST BE >= 0 AND <= 24 HOURS.

*/

   function Check_Time($hhmmss)
{

// READ STANDARD hh:mm:ss TIME STRING ARGUMENT.
// TIME ELEMENTS MUST NOT SUM TO > 24 HOURS AND
// MUST NOT BE A NEGATIVE VALUE.

   $w = HMS_to_Hours($hhmmss);

// CHECK TIME FOR VALIDITY AND RETURN TRUE/FALSE.
// TRUE  = OK
// FALSE = INVALID TIME (< 0 or > 24 HOURS)

   return ($w < 0 or $w > 24)? FALSE:TRUE;

} // END OF  Check_Time(hhmmss)





/*
   ========================================================
   THIS FUNCTION TESTS A GIVEN LATITUDE STRING FOR VALIDITY
   AND RETURNS A BOOLEAN STATE VALUE (TRUE/FALSE).
   TRUE  = OK
   FALSE = INVALID LATUTUDE VALUE

   VALID RANGE IS FROM -90 TO +90 DEGREES.  ANYTHING OUTSIDE
   THIS VALID RANGE RETURNS FALSE.

   LATITUDE ARGUMENT CAN BE DECIMAL DEGREES OR A DMS STRING.
*/
   function Check_Lat($LatStr)
  {
   $w = DMS_to_Deg($LatStr);

   return ($w < -90 or $w > 90)? FALSE:TRUE;
  }



/*
   =========================================================
   THIS FUNCTION TESTS A GIVEN LONGITUDE STRING FOR VALIDITY
   AND RETURNS A BOOLEAN STATE VALUE (TRUE/FALSE).
   TRUE  = OK
   FALSE = INVALID LONGITUDE

   VALID RANGE IS FROM -360 TO +360 DEGREES.  ANYTHING OUTSIDE
   THIS VALID RANGE RETURNS FALSE.

   LONGITUDE ARGUMENT CAN BE DECIMAL DEGREES OR A DMS STRING.
*/
   function Check_Lon($LonStr)
  {
   $w = DMS_to_Deg($LonStr); return ($w < -360 or $w > 360)? FALSE:TRUE;
  }















   function Interp_Lon ($PhaseInterpTable, $JDTTArg)
{
   $wArray = preg_split("[\n]", trim($PhaseInterpTable));
   $wCount = count($wArray);

   $JDArg = floatval($JDTTArg);

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

   list($CurrJDTT, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);
   list($PrevJDTT, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $diff = $CurrPhaseDeg - $PrevPhaseDeg;

   if ($diff < 0)
      {
       $CurrPhaseDeg += 360;
       $wArray[$i] = "$CurrJDTT $CurrPhaseDeg\n";
      }
   }

// Construct final interpolation table ready for use.
   $w = '';
   for ($i=0;   $i < $wCount;   $i++)
   {
   $w .= trim($wArray[$i]) . "\n";
   }
   $PhaseInterpTableStr = trim($w);

// Perform interpolation for phase angle corresponding to JD argument.
   $PhaseDeg = Lagrange_interp($PhaseInterpTableStr, $JDArg);

   $PhaseDeg -= ($PhaseDeg > 360)? 360:0;

   return sprintf("%1.5f", $PhaseDeg);

} // End of  Interp_Lon(...)



// -----------------------------------------------------------
// IDENTICAL TO Interp_Lon() TO REPLACE IT IN FUTURE PROGRAMS.

   function Interp_Angle ($PhaseInterpTable, $JDTTArg)
{
   $wArray = preg_split("[\n]", trim($PhaseInterpTable));
   $wCount = count($wArray);

   $JDArg = floatval($JDTTArg);

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

   list($CurrJDTT, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);
   list($PrevJDTT, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $diff = $CurrPhaseDeg - $PrevPhaseDeg;

   if ($diff < 0)
      {
       $CurrPhaseDeg += 360;
       $wArray[$i] = "$CurrJDTT $CurrPhaseDeg\n";
      }
   }

// CONSTRUCT INTERPOLATION TABLE.
   $w = '';
   for ($i=0;   $i < $wCount;   $i++)
   {
   $w .= trim($wArray[$i]) . "\n";
   }
   $PhaseInterpTableStr = trim($w);

// -------------------------------------------------------------------
// PERFORM INTERPOLATION FOR PHASE ANGLE CORRESPONDING TO JD ARGUMENT.

   $PhaseDeg = Lagrange_Interp($PhaseInterpTableStr, $JDArg);

   $PhaseDeg -= ($PhaseDeg > 360)? 360:0;

   return sprintf("%1.5f", $PhaseDeg);

} // End of  Interp_Angle(...)






/*
  THIS FUNCTION RETURNS AN HTML TEXTAREA READY FOR DISPLAY
  ACCORDING TO THE GIVEN PARAMETERS.

*/
   function Text_Area ($TextBlock, $TxtRows='', $TxtCols=81, $ColorStr='black', $BGColorStr='LightYellow')
 {
   $rows     = intval($TxtRows);
   $cols     = intval($TxtCols);
   $C        = trim($ColorStr);
   $B        = trim($BGColorStr);

   if ($cols <= 0)        {$cols = 1;}
   if ($ColorStr   == '') {$ColorStr = 'black';}
   if ($BGColorStr == '') {$BGColorStr = 'white';}

   if (intval($rows) == 0) {$rows = 2 + substr_count($TextBlock, "\n");}

// -----------------------------
// CONSTRUCT HTML TEXTAREA BLOCK

   return
"<textarea rows='$rows' cols='$cols' readonly='readonly'
style='color:$ColorStr; background-color:$BGColorStr; font-family:monospace;
font-size:18px; font-weight:bold; border:1px solid black; padding:6px;'>
$TextBlock
</textarea>";

 } // end of  Text_Area(...)



/*
  THIS FUNCTION RETURNS AN HTML TEXTAREA TO DISPLAY PHP
  PROGRAMMING CODE ACCORDING TO THE GIVEN PARAMETERS.

*/
   function Text_Area_PHP ($TextBlock, $TxtRows='', $TxtCols=81, $ColorStr='black', $BGColorStr='white')
 {
   $rows     = intval($TxtRows);
   $cols     = intval($TxtCols);
   $C        = trim($ColorStr);
   $B        = trim($BGColorStr);

   if ($cols <= 0)        {$cols = 1;}
   if ($ColorStr   == '') {$ColorStr = 'black';}
   if ($BGColorStr == '') {$BGColorStr = 'white';}

   if (intval($rows) == 0) {$rows = 2 + substr_count($TextBlock, "\n");}

// -----------------------------
// CONSTRUCT HTML TEXTAREA BLOCK

   return
"<textarea rows='$rows' cols='81' readonly='readonly'
style='color:$ColorStr; background-color:$BGColorStr; font-family:monospace;
font-size:18px; font-weight:bold; border:2px solid red; padding:6px;'>
$TextBlock
</textarea>";

 } // end of  Text_Area_PHP(...)







/*

COMPUTE USA DAYLIGHT SAVING TIME START/END DATES.

VALID YEAR RANGE: 1975 to 9999

RETURNS A STRING LIKE:
2017 Mar 12 Sun to 2017 Nov 05 Sun = 238 days = 34 weeks = 2457825 to 2458063

DEPENDING ON THE YEAR, THREE DIFFERENT RULES ARE USED.

*/

   function USA_DST ($year)
{
   $y = intval($year);

   if ($y < 1975 or $y > 9999)
      {return "ERROR: Invalid Year '$y'.  Valid range: 1975 to 9999";}
   if ($y > 1974 and $y < 1987)
  {
   $start = Last_DoW_of_mY (0,  4, $y); // Last Sunday in April
   $end   = Last_DoW_of_mY (0, 10, $y); // Last Sunday in October
  }
   if ($y > 1986 and $y < 2007)
  {
   $start = Nth_DoW_of_mY  (1,  0, 4, $y); // First Sunday in April
   $end   = Last_DoW_of_mY (0, 10, $y);    // Last Sunday in October
  }
   if ($y > 2006)
  {
   $start = Nth_DoW_of_mY (2,  0,  3, $y); // Second Sunday in March
   $end   = Nth_DoW_of_mY (1,  0, 11, $y); // First Sunday in November
  }

   $JDNumStart = 0 + DTZ_to_JD("$start 12");
   $StartDate  = JD_to_Y_Mmm_dn_DoW($JDNumStart);
   $JDNumEnd   = 0 + DTZ_to_JD("$end 12");
   $EndDate    = JD_to_Y_Mmm_dn_DoW($JDNumEnd);
   $DaysSpan   = $JDNumEnd - $JDNumStart;
   $WeeksSpan  = $DaysSpan / 7;

   return "$StartDate to $EndDate = $DaysSpan days = $WeeksSpan weeks = $JDNumStart to  $JDNumEnd ";

} // end of  USA_DST(...)





/*

COMPUTE EUROPEAN UNION SUMMER TIME START/END DATES.

RETURNS A STRING LIKE:
2017 Mar 26 Sun to 2017 Oct 29 Sun = 217 days = 31 weeks = 2457839 to 2458056

*/
   function EU_ST ($year)
{
   $y = intval($year);

   if ($y < 2002 or $y > 9999)
      {return "ERROR: Invalid Year '$y'.  Valid range: 2002 to 9999";}

   $start = Last_DoW_of_mY (0,  3, $y); // Last Sunday in March
   $end   = Last_DoW_of_mY (0, 10, $y); // Last Sunday in October

   $JDNumStart = 0 + DTZ_to_JD("$start 12");
   $StartDate  = JD_to_Y_Mmm_dn_DoW($JDNumStart);
   $JDNumEnd   = 0 + DTZ_to_JD("$end 12");
   $EndDate    = JD_to_Y_Mmm_dn_DoW($JDNumEnd);
   $DaysSpan   = $JDNumEnd - $JDNumStart;
   $WeeksSpan  = $DaysSpan / 7;

   return "$StartDate to $EndDate = $DaysSpan days = $WeeksSpan weeks = $JDNumStart to  $JDNumEnd ";

}










   FUNCTION _Lunar_Ephemeris_Table ($yyyymn)
{
   $w    = substr($yyyymn,0,6);
   $yyyy = intval(substr($w,0,4));
   $m    = intval(substr($w,4,2));

// Determine name of month.
   $MonthName = trim(substr("January  February March    April    May      June     July     August   SeptemberOctober  November December", 9*($m-1), 9));

   $DataArray = file("data/DE405_Moon_Geo_Equ_Coords_1600_to_2200.data");
   $DataLines = count($DataArray);

   $mDays = cal_days_in_month(CAL_GREGORIAN, $m, $yyyy);

// Compute JD number for first day of Year/Month
   $JDNum1x10 = 10 * GregorianToJD($m, 1, $yyyy);


// Find index (i) of (JDNum x 10) within DataArray
   for($i=0;   $i < $DataLines;   $i++)
  {
   $JDTTx10 = substr($DataArray[$i], 0,8);
              if ($JDTTx10 == $JDNum1x10) {break;}
  }

// -----------------------------------------------------------
// Generate lunar ephemeris angle table for month with padding
// overlap for interpolation near ends of table.
//
//   $LunarEphemerisTable = '';  $k = -3;
//   for ($j = $i-5;   $j < $i + 2*$mDays + 3;   $j++)

   $LunarEphemerisTable = '';  $k = -4;

   for ($j = $i-6;   $j < $i + 2*$mDays + 4;   $j++)
  {
   $DataLine = $DataArray[$j];

   $JDTT = floatval(substr($DataLine,0,8)) / 10;
   $JDTT = sprintf("%7.1f", $JDTT);

   $RAHrs = substr($DataLine, 8, 9);
   $RAHrs = substr($RAHrs, 0, 2) . '.' . substr($RAHrs, 2, strlen($RAHrs));
   $RAHrs = substr("          " . sprintf("%2.7f", $RAHrs), -10);

   $DeclDeg = substr($DataLine, 17, 9);
   $DeclDeg = substr($DeclDeg, 0, 3) . '.' . substr($DeclDeg, 3, strlen($DeclDeg));
   $DeclDeg = substr("          " . sprintf("%+2.6f", $DeclDeg), -10);

   $DistKm = substr($DataLine, 26, 9);
   $DistKm = substr($DistKm, 0, 6) . '.' . substr($DistKm, 6, strlen($DistKm));

   $PhaseAng = substr($DataLine, 35, 8);
   $PhaseAng = substr($PhaseAng, 0, 3) . '.' . substr($PhaseAng, 3, strlen($PhaseAng));
   $PhaseAng = substr("         " . sprintf("%3.5f", $PhaseAng), -9);

   $day = ($k < 1.0 or $k >= 2*$mDays+1)? 'xxxx' : ($k+1)/2;

   if ($day <> 'xxxx')
      {
       $day = sprintf("%2.1f", $day);  if ($day < 10) {$day = " $day";}
      }

   $LunarEphemerisTable .= "$JDTT  $RAHrs  $DeclDeg  $DistKm  $PhaseAng   $day\n"; $k++;
  }

// Return raw table without any headers/footers.
   return trim($LunarEphemerisTable);

} // End of  _Lunar_Ephemeris_Table(...)



   function Lunar_Ephemeris_Table ($yyyymn)
{
   $w    = substr($yyyymn, 0,6);
   $yyyy = intval(substr($w, 0,4));
   $m    = intval(substr($w, 4,2));

// Determine name of month.
   $mName = trim(substr("January  February March    April    May      June     July     August   SeptemberOctober  November December", 9*($m-1), 9));

   $mDays = cal_days_in_month(CAL_GREGORIAN, $m, $yyyy);

   $LunarEphemerisTable = _Lunar_Ephemeris_Table ($yyyymn);

   return
"Geocentric Lunar Ephemeris Table - True Equinox and Ecliptic of Date
For $yyyy $mName 1 to $mDays  at 00:00 and 12:00 TT on Each Date

  JDTT        RAHrs      DeclDeg     DistKm    PhaseDeg    mDay
$LunarEphemerisTable
  JDTT        RAHrs      DeclDeg     DistKm    PhaseDeg    mDay";

} // End of  Lunar_Ephemeris_Table(...)





   function Lunar_Ephemeris_TableX ($yyyymn)
{
   $w    = substr($yyyymn,0,6);
   $yyyy = intval(substr($w,0,4));
   $m    = intval(substr($w,4,2));

// Determine name of month.
   $MonthName = trim(substr("January  February March    April    May      June     July     August   SeptemberOctober  November December", 9*($m-1), 9));

   $DataArray = file("data/DE405_Moon_Geo_Equ_Coords_1600_to_2200.data");
   $DataLines = count($DataArray);

   $mDays = cal_days_in_month(CAL_GREGORIAN, $m, $yyyy);

// COMPUTE JD NUMBER FOR FIRST DAY OF YEAR/MONTH
   $JDNum1x10 = 10 * GregorianToJD($m, 1, $yyyy);


// Find index (i) of (JDNum x 10) within DataArray
   for($i=0;   $i < $DataLines;   $i++)
  {
   $JDTTx10 = substr($DataArray[$i], 0,8);
              if ($JDTTx10 == $JDNum1x10) {break;}
  }

// -----------------------------------------------------------
// Generate lunar ephemeris angle table for month with padding
// overlap for interpolation near ends of table.
//
//   $LunarEphemerisTable = '';  $k = -3;
//   for ($j = $i-5;   $j < $i + 2*$mDays + 3;   $j++)

   $LunarEphemerisTable = '';  $k = -7;

   for ($j = $i-9;   $j < $i + 2*$mDays + 7;   $j++)
  {
   $DataLine = $DataArray[$j];

   $JDTT = floatval(substr($DataLine,0,8)) / 10;
   $JDTT = sprintf("%7.1f", $JDTT);

   $RAHrs = substr($DataLine, 8, 9);
   $RAHrs = substr($RAHrs, 0, 2) . '.' . substr($RAHrs, 2, strlen($RAHrs));
   $RAHrs = substr("          " . sprintf("%2.7f", $RAHrs), -10);

   $DeclDeg = substr($DataLine, 17, 9);
   $DeclDeg = substr($DeclDeg, 0, 3) . '.' . substr($DeclDeg, 3, strlen($DeclDeg));
   $DeclDeg = substr("          " . sprintf("%+2.6f", $DeclDeg), -10);

   $DistKm = substr($DataLine, 26, 9);
   $DistKm = substr($DistKm, 0, 6) . '.' . substr($DistKm, 6, strlen($DistKm));

   $PhaseAng = substr($DataLine, 35, 8);
   $PhaseAng = substr($PhaseAng, 0, 3) . '.' . substr($PhaseAng, 3, strlen($PhaseAng));
   $PhaseAng = substr("         " . sprintf("%3.5f", $PhaseAng), -9);

   $day = ($k < 1.0 or $k >= 2*$mDays+1)? 'xxxx' : ($k+1)/2;

   if ($day <> 'xxxx')
      {
       $day = sprintf("%2.1f", $day);  if ($day < 10) {$day = " $day";}
      }

   $LunarEphemerisTable .= "$JDTT  $RAHrs  $DeclDeg  $DistKm  $PhaseAng   $day\n"; $k++;
  }
   $LunarEphemerisTable = trim($LunarEphemerisTable);

   return
"Geocentric Lunar Ephemeris Table - True Equinox and Ecliptic of Date
For $yyyy $MonthName 1 to $mDays  at 00:00 and 12:00 TT on Each Date

  JDTT        RAHrs      DeclDeg     DistKm    PhaseDeg    mDay
$LunarEphemerisTable
  JDTT        RAHrs      DeclDeg     DistKm    PhaseDeg    mDay";

} // End of  Lunar_Ephemeris_TableX(...)





   function Lunar_Altitude_Table ($JDNumTT, $LETableTTStr, $Lat=0, $Lon=0, $Elev=0)
{
// Compute central JD00TT value for altitude table.
   $JD00TT = $JDNumTT - 0.5;

   $LatDeg = DMS_to_Deg($Lat);
   $LonDeg = DMS_to_Deg($Lon);
   $elev   = floatval($Elev);

   $AltTableTTStr = '';

   $MinHr = -24;
   $MaxHr = +48;

   for ($hr = $MinHr;   $hr <= $MaxHr;   $hr++)
  {
   $JDhrTT = $JD00TT + $hr/24;
   $LEStatsStr = LE_Stats ($JDhrTT, $LETableTTStr, $LatDeg, $LonDeg, $elev);

   list
  (
   $JDhrTT, $q,$q,$q,$q,$q,$q,$q,$q,$q,$q,$q,$q,$q, $TopoAltDeg
  ) = preg_split("[ ]", strip_wspace($LEStatsStr));

   $JDhrTT = sprintf("%7.8f", $JDhrTT);
   $TopoAltDeg = substr("          ".sprintf("%+2.6f", $TopoAltDeg), -10);
   $hh = sprintf("%+02d", $hr);
   if ($hr < $MinHr-12 or $hr > $MaxHr+12) {$hh = 'xxxxx';}

   $AltTableTTStr .= "$TopoAltDeg $JDhrTT $hh\n";
  }
   $AltTableTTStr = RTrim($AltTableTTStr);

   return $AltTableTTStr;

} // End of  Lunar_Altitude_Table (...)

















   function LE_Stats ($JDTT, $LunarEphemTableTT, $lat=0, $lon=0, $elevation=0)
{
   $JD = floatval($JDTT);

   $LatDeg = floatval(DMS_to_Deg($lat));
   $LonDeg = floatval(DMS_to_Deg($lon));
   $elev   = floatval($elevation);

// Compute basic lunar ephemeris statistics for JDTT within month table.
   $LunarEphemStats = Lunar_Ephem_Stats ($JDTT, $LunarEphemTableTT);

// Parse and extract all the basic geocentric lunar ephemeris statistics.
// All subsequent computations will be based on these statistics.

   list
  (
   $JDTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg
  ) = preg_split("[ ]", strip_wspace($LunarEphemStats));

   $RADeg = $RAHrs * 15;

// Compute elements required for topocentric computations.
   $ii = 6356755/6378140;
   $jj = floatval($elev)/6378140;
   $kk = atan($ii*tand($LatDeg)); // Radians

   $RhoxSinPhiPrime = $ii*sin($kk) + $jj*sind($LatDeg);
   $RhoxCosPhiPrime = cos($kk) + $jj*cosd($LatDeg);

   $RhoxSinPhiPrime = sprintf('%+1.10f', $RhoxSinPhiPrime);
   $RhoxCosPhiPrime = sprintf('%+1.10f', $RhoxCosPhiPrime);


// -----------------------------------------------------
// Compute nutation in RA (= equation of the equinoxes).
   $dRADeg = dEquinox($JDTT);

// ---------------------------------------------------------------
// Compute local true sidereal time for given date/time/longitude.
// Longitude Neg/Pos = West/East
   $LSTDeg = Mean_Sid_Time($JDTT, $LonDeg) + $dRADeg;
   $LSTHr  = $LSTDeg / 15;

// ----------------------------------------------
// Compute local hour angle in degrees and hours.
   $LHADeg  = $LSTDeg - $RADeg;
   $LHAHrs   = $LHADeg / 15;

// ---------------------------------------
// Compute equatorial horizontal parallax.
   $HPDeg = rad2deg(asin(6378.137 / $DistKm));

// -------------------------------------------
// Compute illuminated fraction of lunar disk.
   $IllumFrac = 1 - 0.5*(1 + cos(deg2rad($PhaseDeg)));
   $IllumFrac = sprintf("%2.6f", $IllumFrac);
   $IllumPercent  = sprintf("%3.2f", 100*$IllumFrac);

// --------------------------------------------------------
// Compute topocentric RA, declination coordinates
// of the moon for the given geographic location.


// Compute topocentric RA.
   $DiffRADeg = atand((-$RhoxCosPhiPrime*sind($HPDeg)*sind($LHADeg)) / (cosd($DeclDeg) - $RhoxCosPhiPrime*sind($HPDeg)*cosd($LHADeg)));
   $TopoRAHrs = $RAHrs + $DiffRADeg/15;
   $TopoRADeg = 15*$TopoRAHrs;

// Compute topocentric declination.
   $TopoDeclDeg = atand(((sind($DeclDeg) - $RhoxSinPhiPrime*sind($HPDeg))*cosd($DiffRADeg))
                / (cosd($DeclDeg) - $RhoxCosPhiPrime*sind($HPDeg)*cosd($LHADeg)));

// Compute local topocentric azimuth and altitude.
   $TopoAzimAltDeg = Azim_Alt($JDTT, $TopoRAHrs, $TopoDeclDeg, $LonDeg, $LatDeg);
   list($TopoAzimDegN0, $TopoAltDeg) = preg_split("[ ]", $TopoAzimAltDeg);

// Compute alternate clockwise azimuth for South = 0 degrees.
   $TopoAzimDegS0 = $TopoAzimDegN0 + 180;
   $TopoAzimDegS0 -= (($TopoAzimDegS0 > 360)? 360:0);


// Determine azimuth direction symbol (E, NNE, SW, etc).
   $AzimDirSymbol = Compass_Symbol($TopoAzimDegN0);

// Compute geocentric lunar angular diameter.
   $AngDiamDeg = 360*atan(1738.1 / $DistKm) / M_PI;


// -----------------------------------------------------------------------
// Compute parallactic angle of the moon from its topocentric coordinates.

   $ParallAngDeg = atand2(sind($LHADeg), (tand($LatDeg)*cosd($TopoDeclDeg) - sind($LatDeg)*cosd($LHADeg)));

   $ParallAngDeg -= ($ParallAngDeg > 180)? 360:0;
   $ParallAngDeg -= ($ParallAngDeg == 90)? 90:0;

// at rise/set:    cos(PA) = sin(lat) / cos(decl)
// $ParallAngDeg = asind(sind($LatDeg) / cosd($DeclDeg));



// Format some values for neatness.
   $JDTT          = sprintf("%7.8f",  $JDTT);
   $HPDeg         = sprintf("%1.6f",  $HPDeg);
   $AngDiamDeg    = sprintf("%1.7f",  $AngDiamDeg);
   $ParallAngDeg  = sprintf("%+3.3f", $ParallAngDeg);
   $LHADeg        = sprintf("%+3.6f", $LHADeg);
   $TopoRAHrs     = sprintf("%2.7f",  $TopoRAHrs);
   $TopoDeclDeg   = sprintf("%+2.6f", $TopoDeclDeg);
   $TopoAltDeg    = sprintf("%+2.6f", $TopoAltDeg);
   $TopoAzimDegS0 = sprintf("%3.3f",  $TopoAzimDegS0);
   $TopoAzimDegN0 = sprintf("%3.3f",  $TopoAzimDegN0);

// Return computed statistics in a space-delimited string.
   return "$JD $RAHrs $DeclDeg $DistKm $PhaseDeg $AngDiamDeg $IllumPercent $LHADeg $ParallAngDeg $HPDeg $TopoRAHrs $TopoDeclDeg $TopoAzimDegN0 $AzimDirSymbol $TopoAltDeg";

} // End of  LE_Stats (...)






   function Mark_Lunar_RS_Points ($RiseSetTableTTStr)
{
   $wArray = preg_split("[\n]", RTrim($RiseSetTableTTStr));
   $wCount = count($wArray);


   for($i=3;   $i < $wCount-3;   $i++)
  {
   $PrevSign = substr(trim($wArray[$i-1]), 0,1);
   $CurrSign = substr(trim($wArray[$i-0]), 0,1);

   if ($CurrSign <> $PrevSign)
  {
   if ($CurrSign == '+')
      {$wArray[$i] = RTrim($wArray[$i])." R\n";}
   else
      {$wArray[$i] = RTrim($wArray[$i])." S\n";}
  }
  } // End of for(i ...)

// Reconstruct marked Rise/Set table
   $w = '';

   for($i=0;   $i < $wCount;   $i++)
   {
   $w .= RTrim($wArray[$i])."\n";
   }

   return RTrim($w);

} // End of  Mark_Lunar_RS_Points(...)









   function Interp_Lunar_RS_Times_TT ($RSTableStrTT)
{

// Store marked altitude table in working array
// and count the lines.
   $wArray = preg_split("[\n]", RTrim($RSTableStrTT));
   $wCount = count($wArray);

// Interpolate JDTT values of all marked Rise/Set
// events and construct them into a table string.
   $RSTableTTStr = '';

   for ($i=3;   $i <= $wCount-3;   $i++)
  {
// Search for Rise/Set point
   $CurrLine = strip_wspace($wArray[$i]);
   $RSMarker = strtoupper(substr($CurrLine, -1));

// If R/S point found, then interpolate
// the JDTT value of the event.
   if ($RSMarker == 'R' or $RSMarker == 'S')
      {
// Create 7 point interpolation table.
   $InterpTableStr = '';

   for ($j = $i-3;   $j < $i+3;   $j++)
  {
   list($AltDeg, $JD) = preg_split("[ ]", strip_wspace($wArray[$j]));
   $InterpTableStr .= "$AltDeg $JD\n";
  }

// Interpolate the JDTT of the event, compute
// date string and append to R/S table.
   $JDTTEvent = sprintf("%7.8f", Lagrange_Interp($InterpTableStr, -0.8333));

// Construct date/time string TT.
   $DateTimeTTStr = JD_To_Date_Time_String ($JDTTEvent);
   $RSTableTTStr .= "$JDTTEvent $DateTimeTTStr TT $RSMarker\n";
  }

   }

   return RTrim($RSTableTTStr);

} // End of  Interp_Lunar_RS_Times_TT (...)






   function Interp_Lunar_RS_Times_XX ($RSTableStrTT, $TZhhmm='00:00', $TimeMode='TT')
{
   global $dTFracDay;

   $dTFrac = $dTFracDay;

   $TZhm = $TZhhmm;

   if (strtoupper($TimeMode) == 'TT')
  {
   $dTFrac = 0;
   $TZhm   = '+00:00';
  }

   if (strtoupper($TimeMode) == 'UT')
  {
   $TZhm   = '+00:00';
  }


// Store marked altitude table in working array
// and count the lines.
   $wArray = preg_split("[\n]", RTrim($RSTableStrTT));
   $wCount = count($wArray);

// Interpolate JDTT values of all marked Rise/Set
// events and construct them into a table string.
   $RSTableTTStr = '';

   for ($i=3;   $i <= $wCount-3;   $i++)
  {
// Search for Rise/Set point
   $CurrLine = strip_wspace($wArray[$i]);
   $RSMarker = strtoupper(substr($CurrLine, -1));

// If R/S point found, then interpolate
// the JDTT value of the event.
   if ($RSMarker == 'R' or $RSMarker == 'S')
      {
// Create 7 point interpolation table.
   $InterpTableStr = '';

   for ($j = $i-3;   $j < $i+3;   $j++)
  {
   list($AltDeg, $JD) = preg_split("[ ]", strip_wspace($wArray[$j]));
   $InterpTableStr .= "$AltDeg $JD\n";
  }

// Interpolate the JDTT of the event, compute
// date string and append to R/S table.
   $JDTTEvent = sprintf("%7.8f", Lagrange_Interp($InterpTableStr, -0.8333));
// Construct date/time string TT.
//   $DateTimeTTStr = JD_To_Date_Time_String ($JDTTEvent);
   $DateTimeTTStr = JD_Z_to_DT ($JDTTEvent - $dTFrac, $TZhm, 2);

   $RSTableTTStr .= "$JDTTEvent $DateTimeTTStr $TimeMode $RSMarker\n";
  }

   }

   return RTrim($RSTableTTStr);

} // End of  Interp_Rise_Set_Times_XX (...)





   function Daily_Lunar_Rise_Set_Table ($yyyymn, $TZhhmm, $TimeMode, $LatDeg, $LonDeg, $Elev)
{
// Compute JD number TT for 1st day of month
   $yyyy = intval(substr($yyyymn,0,4));
   $mn   = intval(substr($yyyymn,4,2));

   $JDNumTT0th = GregorianToJD($mn, 1, $yyyy) - 1;

   $LETableTT = lunar_ephemeris_tableX($yyyymn);

// Cycle through each day of the month from 1 to mDays.
   $mDays = cal_days_in_month(CAL_GREGORIAN, $mn, $yyyy);

   $out = '';

   for ($dd=1;   $dd <= $mDays;   $dd++)
 {
   $wJDNumTT = $JDNumTT0th + $dd;

   $AltTableTTStr     = Lunar_Altitude_Table ($wJDNumTT, $LETableTT, $LatDeg, $LonDeg, $Elev);
   $RSTableTTStr      = Mark_Lunar_RS_Points ($AltTableTTStr);
   $RiseSetTableLTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, $TZhhmm, $TimeMode);

//   $RiseSetTableUTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, '+00:00', 'UT');
//   $RiseSetTableTTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, '+00:00', 'TT');

// Filter LT Rise/Set table keeping only lines matching current date.
   $wArray = preg_split("[\n]", $RiseSetTableLTStr);
   $wCount = count($wArray);
   $w = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = $wArray[$i];
   if(intval(substr($CurrLine, 26,2)) == intval($dd)) {$w .= "$CurrLine\n";}
  }

// Tidy up Rise/Set text.
   $wArray = preg_split("[\n]", $w);
   $wCount = count($wArray);
   $u = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = RTrim($wArray[$i]);
   $A = substr($CurrLine, 17, 15);
   $B = substr($CurrLine, 33, 8);
   $B = Round_HMS($B, 'hm');
   $C = substr($CurrLine, -1);
   $C = "Moon" . (($C == 'R')? 'rise ':'set  ');
   $u .= "$C $A $B $TimeMode\n";
  }

// Filter to create blank date separator lines.
   $w = RTrim($u);   $u = "Moonset    00:00";
   $w = str_replace("$u LT", "", $w);
   $w = str_replace("$u UT", "", $w);
   $w = str_replace("$u TT", "", $w);

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

   return RTrim($out);

} // End of  Daily_Lunar_Rise_Set_Table (...)





/*
   This function returns a string describing the phase of the
   moon corresponding to the given phase angle argument.  It
   may need some tweaking to be more in line with observation.

   FALSE is returned if angle is outside valid range 0 to 360.


   Is that some form of trick question?

   a > 0 bug fixed 2016 Aug 20 Sun
   changed to
   a >= 0

*/

   function Lunar_Phase_to_Text ($PhaseDeg)
{
   $a = round($PhaseDeg, 0);

   if ($a >= 0   and $a <=   9) {return "New moon";}
   if ($a >  9   and $a <=  24) {return "Waxing crescent, just past new moon";}
   if ($a >  24  and $a <=  79) {return "Waxing crescent moon";}
   if ($a >  79  and $a <=  88) {return "Waxing crescent, approaching first quarter";}
   if ($a >  88  and $a <= 101) {return "First quarter moon";}
   if ($a >  101 and $a <= 110) {return "Waxing gibbous, just past first quarter";}
   if ($a >  110 and $a <= 159) {return "Waxing gibbous moon";}
   if ($a >  159 and $a <= 170) {return "Waxing gibbous, approaching full moon";}
   if ($a >  170 and $a <= 192) {return "Full moon";}
   if ($a >  192 and $a <= 198) {return "Waning gibbous, just past full moon";}
   if ($a >  198 and $a <= 250) {return "Waning gibbous moon";}
   if ($a >  250 and $a <= 260) {return "Waning gibbous, approaching last quarter";}
   if ($a >  260 and $a <= 271) {return "Last quarter moon";}
   if ($a >  271 and $a <= 274) {return "Waning crescent, just past last quarter";}
   if ($a >  274 and $a <= 330) {return "Waning crescent moon";}
   if ($a >  330 and $a <= 345) {return "Waning crescent, approaching new moon";}
   if ($a >  345 and $a <= 360) {return "New moon";}

   return FALSE; // Error if given angle not in valid range.

} // End of Lunar_ Phase_to_Text(...)







/*
  XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  Seeks and marks all New Moons within
  monthly lunar ephemeris table.

  First data line at index i = 4
  Final data line at index i = wLines - 2

*/
  function Mark_Lunar_Quarter_Phase_Points ($LETableStr)
{
  $TableString = trim($LETableStr);

  $TableArray = preg_split("[\n]", $TableString);
  $TableLines = count($TableArray);

// ------------------------------------------
// Compute number of days in the given month.

   $mDays = ($TableLines - 7) / 2 - 3;

// ------------------------------------------------
// Scan table for all New Moon points within month.

   for ($i=7;   $i <= $TableLines-4;   $i++)
  {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q,$q, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$i]);
   list($q,$q,$q,$q, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q,$q, $NextPhaseDeg) = preg_split("[ ]", $NextLine);

   $P = $CurrPhaseDeg - $PrevPhaseDeg;
   $Q = $NextPhaseDeg - $CurrPhaseDeg;

// If New Moon point, then mark the table line. Otherwise, ignore it.
   if ($P < 0 and $Q > 0) {$TableArray[$i] = trim($TableArray[$i]) . " NM\n";}
  }





// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Scan table for all First Quarter Moon points within month.

   for ($i=7;   $i <= $TableLines-4;   $i++)
 {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q,$q, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

//   $CurrLine = strip_wspace($TableArray[$i]);
//   list($q,$q,$q,$q, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q,$q, $NextPhaseDeg) = preg_split("[ ]", $NextLine);

   $P = 90 - $PrevPhaseDeg;
   $Q = 90 - $NextPhaseDeg;

// If FQ Moon point, then mark the table line. Otherwise, ignore it.
   if ($P > 0 and $Q < 0)
      {
       $a = abs(90 - $PrevPhaseDeg);
       $b = abs(90 - $NextPhaseDeg);
       if ($a < $b) {$TableArray[$i] = trim($TableArray[$i]) . " FQ\n";}
      }
 }





// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Scan table for all Full Moon points within month.
   for ($i=7;   $i <= $TableLines-4;   $i++)
  {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q,$q, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$i]);
   list($q,$q,$q,$q, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q,$q, $NextPhaseDeg) = preg_split("[ ]", $NextLine);

// -------------------------------------------------------------------
// If Full Moon point, then mark the table line. Otherwise, ignore it.

   $P = 180 - $PrevPhaseDeg;
   $Q = 180 - $CurrPhaseDeg;

   if ($P > 0 and $Q < 0) {$TableArray[$i] = trim($TableArray[$i]) . " FM\n";}
  }





// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Scan table for all Last Quarter Moon points within month.

   for ($i=7;   $i <= $TableLines-4;   $i++)
  {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q,$q, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$i]);
   list($q,$q,$q,$q, $CurrPhase) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q,$q, $NextPhaseDeg) = preg_split("[ ]", $NextLine);

// -----------------------------------------------------------------
// If LQ Moon point, then mark the table line. Otherwise, ignore it.

   $P = 270 - $PrevPhaseDeg;
   $Q = 270 - $CurrPhase;

   if ($P > 0 and $Q < 0) {$TableArray[$i] = trim($TableArray[$i]) . " LQ\n";}
  }

// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Reconstruct final marked table as an output string.

   $OutputTable = '';

   for($i=0;   $i < $TableLines;   $i++)
      {$OutputTable .= Rtrim($TableArray[$i]) . "\n";}

// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Return the marked table.  Same as original input table string, but
// with all quarter phase points marked with their respective codes.

   return Rtrim($OutputTable);

} // End of  Mark_Lunar_Quarter_Phase_Points (...)




   function Mark_Lunar_Apogee_Perigee_Points ($LETableStr)
{
  $TableString = trim($LETableStr);

  $TableArray = preg_split("[\n]", $TableString);
  $TableLines = count($TableArray);

// ----------------------------------------------
// Compute number of days in the tabulated month.
// Does NOT include the 36-hours padding at each
// end of the table

   $mDays = ($TableLines - 7) / 2 - 3;

// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Scan table for first extemum within the month.  This may
// be either an apogee or perigee point.  We need to find
// this first extremum point first.

   $PrevLine = strip_wspace($TableArray[5]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[6]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $P = $CurrDistKm - $PrevDistKm;

   if ($P > 0) {$pattern = 'APA';} else {$pattern = 'PAP';}

// If first distance extremum is an apogee.
   if ($pattern == 'APA')
  {

// Seek first apogee.
   for ($i=7;   $i <= $TableLines-4;   $i++)
  {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$i]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P < 0)  {$TableArray[$i] = Rtrim($TableArray[$i] . " A\n"); break;}
  }
// Seek following perigee.
   for ($j=$i+1;   $j <= $TableLines-4;   $j++)
  {
   $PrevLine = strip_wspace($TableArray[$j-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$j]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$j+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P > 0)  {$TableArray[$j] = Rtrim($TableArray[$j] . " P\n"); break;}
  }

// Seek following apogee.
   for ($k=$j+1;   $k <= $TableLines-4;   $k++)
  {
   $PrevLine = strip_wspace($TableArray[$k-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$k]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$k+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P < 0)  {$TableArray[$k] = Rtrim($TableArray[$k] . " A\n"); break;}
  }

}

 else


  {

// If first distance extremum is a perigee.

// Seek first perigee.
   for ($i=7;   $i <= $TableLines-4;   $i++)
  {
   $PrevLine = strip_wspace($TableArray[$i-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$i]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$i+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P > 0)  {$TableArray[$i] = Rtrim($TableArray[$i] . " P\n"); break;}
  }
// Seek following apogee.
   for ($j=$i+1;   $j <= $TableLines-4;   $j++)
  {
   $PrevLine = strip_wspace($TableArray[$j-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$j]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$j+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P < 0)  {$TableArray[$j] = Rtrim($TableArray[$j] . " A\n"); break;}
  }

// Seek following perigee.
   for ($k=$j+1;   $k <= $TableLines-4;   $k++)
  {
   $PrevLine = strip_wspace($TableArray[$k-1]);
   list($q,$q,$q, $PrevDistKm, $q) = preg_split("[ ]", $PrevLine);

   $CurrLine = strip_wspace($TableArray[$k]);
   list($q,$q,$q, $CurrDistKm, $q) = preg_split("[ ]", $CurrLine);

   $NextLine = strip_wspace($TableArray[$k+1]);
   list($q,$q,$q, $NextDistKm, $q) = preg_split("[ ]", $NextLine);

   $P = $CurrDistKm - $PrevDistKm;
   if ($P > 0)  {$TableArray[$k] = Rtrim($TableArray[$k] . " P\n"); break;}
  }
  }

// -----------------------------------------------------
// Reconstruct final marked table as an output string in
// the same format as the original ephemeris table, but
// with all quarter phase, apogee and perigee points
// marked for subsequent computation of their precise
// dates and times (TT).

   $OutputTable = '';

   for($i=0;   $i < $TableLines;   $i++)
      {$OutputTable .= Rtrim($TableArray[$i]) . "\n";}

// ------------------------------
// Return the final marked table.

   return Rtrim($OutputTable);

} // End of  Mark_Lunar_Apogee_Perigee_Points (...)






/*
Returns basic lunar ephemeris statistics for any
JDTT value within the given ephemeris table.
*/


   function Lunar_Ephem_Stats ($JDTTArg, $LETableStr)
{
// Read JDTT argument corresponding date and time (TT).
   $J = floatval($JDTTArg);

   $wStr = trim($LETableStr);

   $wArray = preg_split("[\n]", $wStr);
   $wCount = count($wArray);

/* Get first and last JD numbers within the month
   to determine the range in which the JDTT value
   falls within the table.  If outside the table,
   then FALSE is returned to indicate the error.
*/

   $JDTTMin = floatval($wArray[9]);
   $JDTTMax = floatval($wArray[$wCount-5]);

   if ($J < $JDTTMin or $J > $JDTTMax) {return FALSE;}

// Given precise JDTT value, find first JDTT value
// within the table greater the given JDTT argument
// and extract a 7-point working table for use with
// Lagrangian interpolation.


   for ($i=9;   $i < $wCount;   $i++)
  {
   $CurrLine = strip_wspace($wArray[$i]);
   list($JDTTTab) = preg_split("[ ]", $CurrLine);

   $JDTTTab = floatval($JDTTTab);

   if ($JDTTTab > $J) {break;}
  }


// Construct 7-point Lagrange interpolation tables for each ephemeris statistic.

  $RAHrTableStr = $DeclDegTableStr = $DistKmTableStr = $PhaseDegTableStr = '';

  for ($j=$i-3;   $j < $i+4;   $j++)
 {
  $CurrLine = strip_wspace($wArray[$j]);

  list($JDTT,$RAHr,$DeclDeg,$DistKm,$PhaseDeg,$mDay) = preg_split("[ ]", $CurrLine);

  $RAHrTableStr     .= "$JDTT $RAHr\n";
  $DeclDegTableStr  .= "$JDTT $DeclDeg\n";
  $DistKmTableStr   .= "$JDTT $DistKm\n";
  $PhaseDegTableStr .= "$JDTT $PhaseDeg\n";
}
  $RAHrTableStr     = trim($RAHrTableStr);
  $DeclDegTableStr  = trim($DeclDegTableStr);
  $DistKmTableStr   = trim($DistKmTableStr);
  $PhaseDegTableStr = trim($PhaseDegTableStr);


// Interpolate RA.
   $RAHr = Interp_Hours($RAHrTableStr, $J);

// Interpolate declination
   $DeclDeg = Lagrange_interp($DeclDegTableStr, $J);

// Interpolate distance km
   $DistKm = Lagrange_interp($DistKmTableStr, $J);

// Interpolate phase angle in degrees
   $PhaseDeg = Interp_Lunar_Phase_Ang($PhaseDegTableStr, $J);

// Format computed statistics.
   $J        = sprintf("%7.8f",  $J);
   $RAHr     = sprintf("%2.7f",  $RAHr);
   $DeclDeg  = sprintf("%+2.6f", $DeclDeg);
   $DistKm   = sprintf("%6.3f",  $DistKm);
   $PhaseDeg = sprintf("%3.5f",  $PhaseDeg);

   $RAHr     = substr("          $RAHr", -10);
   $DeclDeg  = substr("           $DeclDeg", -11);
   $PhaseDeg = substr("         $PhaseDeg", -9);

// Construct and return final ephemeris statistics string.
   return "$J $RAHr $DeclDeg $DistKm $PhaseDeg";

} // End of  Lunar_Ephem_Stats (...)







   function Interp_Lunar_Phase_Ang ($PhaseInterpTable, $JDTTArg)
{
   $wArray = preg_split("[\n]", trim($PhaseInterpTable));
   $wCount = count($wArray);

   $JDArg = floatval($JDTTArg);

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

   list($CurrJDTT, $CurrPhaseDeg) = preg_split("[ ]", $CurrLine);
   list($PrevJDTT, $PrevPhaseDeg) = preg_split("[ ]", $PrevLine);

   $diff = $CurrPhaseDeg - $PrevPhaseDeg;

   if ($diff < 0)
      {
       $CurrPhaseDeg += 360;
       $wArray[$i] = "$CurrJDTT $CurrPhaseDeg\n";
      }
   }

// Construct final interpolation table ready for use.
   $w = '';
   for ($i=0;   $i < $wCount;   $i++)
   {
   $w .= trim($wArray[$i]) . "\n";
   }
   $PhaseInterpTableStr = trim($w);


// Perform interpolation for phase angle corresponding to JD argument.
   $PhaseDeg = Lagrange_interp($PhaseInterpTableStr, $JDArg);

   $PhaseDeg -= ($PhaseDeg > 360)? 360:0;

   return sprintf("%1.5f", $PhaseDeg);

} // End of  Interp_Lunar_Phase_Ang(...)


   function Lunar_Phases_Apogees_Perigees ($yyyymn, $TZhhmm='+00:00', $dTFlag=FALSE)
{
   global $_BAR_ ;

// ----------------------------------
// Read and tidy up TZ, if necessary.

   $TZhhmm = substr(Hours_to_HMS(HMS_to_Hours($TZhhmm), 0, '+', ':'), 0,6);

 /* ----------------------------------------------------
   Compute semi-integer-encoded date (Year/Month only).
   This value is the calling argument for the function
   that will generate the raw lunar ephemeris table.
*/

   $yyyy = intval(substr($yyyymn, 0,4));
   $mn   = intval(substr($yyyymn, 4,2));
   $m    = intval($mn);

// --------------------------------
// Get month name and abbreviation.

   $MonthName = trim(substr("January  February March    April    May      June     July     August   SeptemberOctober  November December", 9*($m-1), 9));
   $Mmm = substr($MonthName, 0,3);

// -------------------------------------------------
// Set Delta T value resolved to the nearest second,
// if indicated by the dTFlag state.

   $dTSeconds = ($dTFlag === TRUE)? round(Delta_T($yyyymn), 0) : 0;
   $dThhmmss  = hours_to_HMS($dTSeconds/3600, 0, '+', ':');

   $dTFracDay = sprintf('%+1.8f', $dTSeconds/86400);

// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// Compute JD number for current date. And determine
// past/present/future interface context.  Is the
// given date in the past, present or future?
   $WereAreWillBe = '';
   $wY = intval(gmdate('Y'));
   $wm = intval(gmdate('m'));

   $JDNum1stNow  = GregorianToJD($wm,1,$wY);
   $JDNum1stThen = DTZ_to_JD ("$yyyy$mn" . "01 12:00");

   if ($JDNum1stThen  < $JDNum1stNow) {$WereAreWillBe = 'were';}    // Past context
   if ($JDNum1stThen == $JDNum1stNow) {$WereAreWillBe = 'are';}     // Present context
   if ($JDNum1stThen  > $JDNum1stNow) {$WereAreWillBe = 'will be';} // Future context

// Patch to use past/present/future context flag, if applicable.
// If used in a series of tables, such as a tables for a full
// year, then this should be set to FALSE.
// Default = TRUE, assuming only a single year usage.
// STILL BUGGY CONTEXT - USE 'are' UNTIL FIXED.
//   if (!$CXFlag) {$WereAreWillBe = "are";}
   $WereAreWillBe = "are"; // FORCE 'are' UNTIL FIXED.

// ---------------------------------------------------------
// Determine interface input time scale interpretation mode.
// For example, if TZ == 0 and Delta T <> 0 then time mode is UT.

   $TimeScalesymbol = $TTZ = $Z = ' ';   $ZMode = '';
   if ($dTFlag === FALSE and $TZhhmm == '+00:00') {$TimeScaleSymbol = ' TT ';  $TimeScalesymbol = '= TT  ';}
   if ($dTFlag === FALSE and $TZhhmm <> '+00:00') {$TimeScaleSymbol = 'TTZ ';  $TimeScalesymbol = '= TTZ '; $TTZ = 'Z'; $Z = 'Z'; $ZMode = '+ TZ';}
   if ($dTFlag === TRUE  and $TZhhmm == '+00:00') {$TimeScaleSymbol = ' UT ';  $TimeScalesymbol = '= UT  ';}
   if ($dTFlag === TRUE  and $TZhhmm <> '+00:00') {$TimeScaleSymbol = ' LT ';  $TimeScalesymbol = '= LT  ';}

// =======================================================================
// Generate raw ephemeris table for month at 12-hour (half-day) intervals.

   $RawLunarTTEphemTableString = Lunar_Ephemeris_Table($yyyymn);

// ================================================================
// First mark all lunar quarter phase points within the month table
// and then mark all apogee and perigee points.
// Quarter phase point markers = NM, FQ, FM, LQ
// Apogee and perigee point markers = A, P

   $MarkedQPhasePointsTable = Mark_Lunar_Quarter_Phase_Points($RawLunarTTEphemTableString);
   $MarkedLunarTTEventsTableString = Mark_Lunar_Apogee_Perigee_Points($MarkedQPhasePointsTable);

// Compute interpolation tables for all quarter phase events within month.
   $QPhaseInterpTablesString = Construct_Lunar_Phase_Interp_Tables($MarkedQPhasePointsTable);

// Compute JD TT tables of all quarter phase, apogee and perigee events within month.
   $QPhaseTTEventsTableString  = Compute_Lunar_Quarter_Phases($RawLunarTTEphemTableString);
   $APTTEventsTableString = Compute_Lunar_Apogee_Perigee($RawLunarTTEphemTableString);

// ****************************************************************************
// Combine both events tables into a single table and sort lines by JDTT value.

   $wArray = preg_split("[\n]", $QPhaseTTEventsTableString . "\n" . $APTTEventsTableString);
   sort($wArray);
   $wCount = count($wArray);
   $w = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $w .= trim($wArray[$i]) . "\n";
  }
   $EventsTTTableString = trim($w);

// Translate TT events table to local time zone.
   $LunarEventsTableString = Lunar_Events_JDTT_to_LT ($EventsTTTableString, $TZhhmm, $dTFlag);


// ****************************************************************************
// Filter out extraneous dates of prior or following months
// due to time zone.  This ONLY applies to lunar events.

   $w = $LunarEventsTableString;

   $wArray = preg_split("[\n]", $w);
   $wCount = count($wArray);

   $w = '';

   $MmmKeep = $Mmm;

   for ($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = $wArray[$i];
   if (strpos($CurrLine, $MmmKeep) !== FALSE)
      {
       if(intval($CurrLine) <> 0){$w .= "$CurrLine\n";}
      }
  }
   $LunarEventsTableString = trim($w);










// ==================================================
// Note any duplicate phase in the same month or the
// lack of a particular quarter phase. For example,
// there may possibly be two Full Moons or two New
// Moons, etc. in the same month or there may not
// be any Full Moons at all in a particular month.

   $w = $LunarEventsTableString;
   $NMCount = substr_count($w, ' New Moon');
   $FQCount = substr_count($w, ' First Quarter');
   $FMCount = substr_count($w, ' Full Moon');
   $LQCount = substr_count($w, ' Last Quarter');

   $DupPhaseNote = '';
   if ($NMCount > 1)  {$DupPhaseNote = "New";}
   if ($FQCount > 1)  {$DupPhaseNote = "First Quarter";}
   if ($FMCount > 1)  {$DupPhaseNote = "Full";}
   if ($LQCount > 1)  {$DupPhaseNote = "Last Quarter";}

   if ($DupPhaseNote <> '')
      {$DupPhaseNote = "\nThere $WereAreWillBe two " . strtolower($DupPhaseNote)." moons in $MonthName of $yyyy ".trim($TimeScaleSymbol).".\n";}

// --------------------------------------
// If any particular phase does NOT occur
// in the given month, then note that too.
   $NoPhaseNote = '';
   if ($NMCount == 0){$NoPhaseNote = "New";}
   if ($FQCount == 0){$NoPhaseNote = "First Quarter";}
   if ($FMCount == 0){$NoPhaseNote = "Full";}
   if ($LQCount == 0){$NoPhaseNote = "Last Quarter";}

   if ($NoPhaseNote <> '')
      {$NoPhaseNote = "\nThere $WereAreWillBe no " . strtolower($NoPhaseNote) . " moons in $MonthName of $yyyy ".trim($TimeScaleSymbol).".\n";}

// Generate blue moon note, if indicated.
   $BlueMoonNote = '';
   $BlueMoonNote = ($FMCount > 1)? "The second full moon is a 'blue moon'.\n":'';

// Generate black moon note, if indicated.
   $BlackMoonNote = '';
   $BlackMoonNote = ($NMCount > 1)? "The second new moon is a 'black moon'.\n":'';

// Get whichever phase note applies, if any.
// Otherwise, the note string is empty.
   $PhaseNotes = "$DupPhaseNote$BlueMoonNote$BlackMoonNote$NoPhaseNote";

// Insert ' also ' into extrema note if lunar events count > 1
   $also = ($PhaseNotes <> '')? ' also':'';

// ==================================================
// Note any etremum in the same month or the lack of
// a particular extremum.  For example, a month with
// either no perigee or apogee or a month with two
// apogees or perigees.
   $w = $LunarEventsTableString;
   $ApogeeCount  = substr_count($w, ' Apogee');
   $PerigeeCount = substr_count($w, ' Perigee');

   $DupExtremumNote = '';
   if ($ApogeeCount  > 1){$DupExtremumNote = "Apogees";}
   if ($PerigeeCount > 1){$DupExtremumNote = "Perigees";}

   $NoExtremumNote = '';
   if ($ApogeeCount  == 0){$NoExtremumNote = "Apogee";}
   if ($PerigeeCount == 0){$NoExtremumNote = "Perigee";}

   if ($DupExtremumNote <> '')
      {$DupExtremumNote = "\nThere $WereAreWillBe$also two lunar " . strtolower($DupExtremumNote) . " in $MonthName of $yyyy " . trim($TimeScaleSymbol) . ".\n";}

   if ($NoExtremumNote <> '')
      {$NoExtremumNote = "\nThere $WereAreWillBe no lunar " . strtolower($NoExtremumNote) . " in $MonthName of $yyyy " . trim($TimeScaleSymbol) . ".\n";}

   $ExtremaNote = "$DupExtremumNote$NoExtremumNote";


   $EventNotes = "$PhaseNotes$ExtremaNote";

   $LunarEventsTableTitleString = LR_X_Pad ("Lunar Events for $MonthName of $yyyy", "R", " ", 46)
   . "Time Mode = $TimeScaleSymbol";




/* ---------------------------------------------------------
   Return table of all the marked lunar events for the given
   Year/Month/Time Zone/Delta T.
*/

$MONTHNAME = strtoupper($MonthName);

$LunarEventsTable =
"$_BAR_
$MONTHNAME $yyyy - Lunar Quarter Phases, Apogees and Perigees
For Time Zone UT$TZhhmm
Applied NASA Delta T = $dThhmmss = $dTFracDay day

    Date and Time$TimeScaleSymbol                Lunar Event               Julian Date TT
------------------------ -----------------------------------  ----------------
$LunarEventsTableString
$EventNotes
";

// Filter events table for content structure
// such as removing redundant labels.
   if ($dThhmmss <> '+00:00:00' and $TZhhmm == '+00:00')
  {
   $LunarEventsTable = str_replace('For Time Zone UT+00:00','UT', $LunarEventsTable);
  }

   if ($dThhmmss == '+00:00:00' and $TZhhmm == '+00:00')
  {
   $LunarEventsTable = str_replace('For Time Zone UT+00:00','TT', $LunarEventsTable);
   $LunarEventsTable = str_replace('Applied NASA Delta T = +00:00:00 = +0.00000000 day','', $LunarEventsTable);
  }

return $LunarEventsTable;

} // End of  Lunar_Phases_Apogees_Perigees(...)



/*
   Given a previously marked ephemeris table, compute
   the JDs (Julian Dates TT) for all of the marked
   phases and collect them into a table.  This table
   of JD TT values can then be translated to any
   local time zone.
*/
   function Construct_Lunar_Phase_Interp_Tables ($MarkedTable)
{
// Initialize.

// Store raw marked table in working array.
   $wArray = preg_split("[\n]", Rtrim($MarkedTable));

// Count total number of lines in the table.
   $TotalLinesCount = count($wArray);

   $PhaseCode = '';

// Get table starting and ending JD values.
   $JDTTStart = floatval($wArray[4]);
   $JDTTEnd   = floatval($wArray[$TotalLinesCount-1]);

   $OutputPhasesArray = array();

   $j = $k = 0;

   $InterpTable = '';

   for($i=0;   $i < $TotalLinesCount;   $i++)
{
   $PhaseCode = $InterpTable = '';

   $CurrLine = strip_wspace($wArray[$i]);

   if (substr($CurrLine, -2) == 'NM')  {$PhaseCode = 'NM';}
   if (substr($CurrLine, -2) == 'FQ')  {$PhaseCode = 'FQ';}
   if (substr($CurrLine, -2) == 'FM')  {$PhaseCode = 'FM';}
   if (substr($CurrLine, -2) == 'LQ')  {$PhaseCode = 'LQ';}


// Perform computations for marked phases and
// collect the JDs in the month phases table.

   switch ($PhaseCode)

  {
   case 'NM':
   $OutputPhasesArray[$j] = Rtrim($wArray[$i]) . " $i\n";

   break;


   case 'FQ':
   $OutputPhasesArray[$j] = Rtrim($wArray[$i]) . " $i\n";

   break;


   case 'FM':
   $OutputPhasesArray[$j] = Rtrim($wArray[$i]) . " $i\n";
   break;


   case 'LQ':
   $OutputPhasesArray[$j] = Rtrim($wArray[$i]) . " $i\n";
   break;



   default: break;

  } // End of Switch

  $j++;
}






   $IndexedPhasesList = $MonthPhasesTable = '';
   $PhasesList = array();

   sort($OutputPhasesArray);

   $LinesCount = count($OutputPhasesArray);

   for ($i=0;   $i < $LinesCount;   $i++)
{
   $w = trim(substr(trim($OutputPhasesArray[$i]), -5));
   $w = trim(substr($w, -2) . " " . substr($w,0,2));
   $IndexedPhasesList .= "$w\n";
}

/*
9 NM
25 FQ
39 FM
52 LQ
68 NM
*/

// Compute JDTTs of indexed phase events.


$PhaseArray = preg_split("[\n]", trim($IndexedPhasesList));
$PhaseCount = count($PhaseArray);

$InterpTable = '';

$NMTableStr = $FQTableStr = $FMTableStr = $LQTableStr = '';

// Loop to compute all indexed phase events.
   for($i=0;   $i < $PhaseCount;   $i++)
{
   $CurrLine = trim($PhaseArray[$i]);
   $j = intval($CurrLine);
   $PhaseSymbol = substr($CurrLine, -2);

// Generate interpolation table for New Moon(s).
   if ($PhaseSymbol == 'NM')
      {
   for ($k = $j-3;   $k < $j+4;   $k++)
       {
        $InterpTable .= @trim($wArray[$k]) . "\n";
       }
      }

// Generate interpolation table for First Quarter Moon(s).
   if ($PhaseSymbol == 'FQ')
      {
   for ($k = $j-3;   $k < $j+4;   $k++)
       {
        $InterpTable .= @trim($wArray[$k]) . "\n";
       }
      }

// Generate interpolation table for Full Moon(s).
   if ($PhaseSymbol == 'FM')
      {
   for ($k = $j-3;   $k < $j+4;   $k++)
       {
        $InterpTable .= @trim($wArray[$k]) . "\n";
       }
      }

// Generate interpolation table for Last Quarter Moon(s).
   if ($PhaseSymbol == 'LQ')
      {
   for ($k = $j-3;   $k < $j+4;   $k++)
       {
        $InterpTable .= @trim($wArray[$k]) . "\n";
       }
      }

}

// Tidy up final combined phases table.
   $InterpTable = Rtrim(substr(trim($InterpTable), 0, strlen($InterpTable)-1));
//   $InterpTable = Rtrim($InterpTable);


// EXPERIMENTAL PATCH to remove any redundant phase marker at end of table.
//   $w = trim(substr($InterpTable, -2));
//   if ($w =='FQ')
//      {$InterpTable = Rtrim(substr($InterpTable, 0, strlen($w)));}


   $LinesCount  = substr_count($InterpTable, "\n") + 1;
   $PhaseCount  = $LinesCount / 7;




// Done. Return JD table of lunar phases.
//   return Rtrim($MonthPhasesTable);
//   return Rtrim($IndexedPhasesList);
   return Rtrim($InterpTable);

} // end of  Construct_Lunar_Phase_Interp_Tables (...)



/*
   Given a raw lunar ephemeris table string, compute a table
   of JDTT (Julian Dates TT) values for all quarter phase
   events within the month.
*/

   function Compute_Lunar_Quarter_Phases ($LETableStr)
{
   $w = Mark_Lunar_Quarter_Phase_Points($LETableStr);

   $w = Construct_Lunar_Phase_Interp_Tables($w);

// Store phases table in working array.
   $wArray = preg_split("[\n]", trim($w));

// Count the total number of lines
// contained in the array.
   $LinesCount = count($wArray);

// Count the number of phases contained in the tables.
// Each phase contains 7 data lines centered on the
// phase marker (NM,FQ,FM,LQ).  The phase count can
// range from 3 to 5 phases in a month.
   $PhaseCount = $LinesCount / 7;


// Interpolate each of the phases.

   $InterpTable = $out = '';

   for($i=0;   $i < $PhaseCount;   $i++)
  {
   $j = 3 + 7*$i;

   $EventSymbol = substr(trim($wArray[$j]), -2);

   $InterpTable = '';
   for($k = $j-3;   $k < $j+4;   $k++)
      {
       $CurrLine = strip_wspace($wArray[$k]);
       list($JDTT,$q,$q,$q, $PhaseAng) = preg_split("[ ]", $CurrLine);
       $InterpTable .= "$PhaseAng $JDTT\n";
      }

// Ready to interpolate the ith phase.
   $InterpTable = trim($InterpTable);





// Handle New Moons differently than the other phases
// because of the 360/0-degree transition problem.
   if ($EventSymbol == 'NM')
  {
   $PhaseAng = 360;

// deconstruct-reconstruct interp table as test.
   $tArray = preg_split("[\n]", $InterpTable);
   $tArrayCount = count($tArray);

   $w = '';

   for($n=0;   $n < $tArrayCount;   $n++)
   {
   $CurrLine = $tArray[$n];
   list($PhaseDeg, $JDTT) = preg_split("[ ]", $CurrLine);
   $PhaseDeg += ($n > 2)? 360:0;
   $w .= "$PhaseDeg $JDTT\n";
   }
   $InterpTable = trim($w);

   $w = Lagrange_interp($w, $PhaseAng);
   $w = sprintf("%7.8f", $w) . " $EventSymbol";
}

else

  {

// Compute JDTT vs Quarter phase angle tables other than New Moons.
   if ($EventSymbol == 'FQ') {$PhaseAng = 90;}
   if ($EventSymbol == 'FM') {$PhaseAng = 180;}
   if ($EventSymbol == 'LQ') {$PhaseAng = 270;}

   // Interpolate the JDTT of the phase and attach phase symbol.
      $w = Lagrange_interp($InterpTable, $PhaseAng);
      $w = sprintf("%7.8f", $w) . " $EventSymbol";
  }



  $out .= "$w\n";


  } // End of  for(i ...)

  return trim("$out");

} // End of Compute_Lunar_Quarter_Phases(...)



/*
   Given a raw lunar ephemeris table for any given Year/Month,
   returns a table of Julian Dates of all apogees and return
   results on TT scale.

*/

   function Compute_Lunar_Apogee_Perigee ($LETableStr)
{
   $w = Mark_Lunar_Apogee_Perigee_Points ($LETableStr);

   $w = trim($w);

   $wArray = preg_split("[\n]", $w);
   $wCount = count($wArray);

   list($q, $yyyy, $MonthName) = preg_split("[ ]", strip_wspace($wArray[1]));

   $YearMonthName = "$yyyy $MonthName";

   $APTable = $w = '';
   for ($i=4;   $i <= $wCount-2;   $i++)
  {
   $CurrLine = trim($wArray[$i]);  $AP = substr($CurrLine, -1);

   if ($AP == 'A' or $AP == 'P')
      {
       for($j = $i-2;    $j < $i+3;    $j++)
       $APTable .= Rtrim($wArray[$j]) . "\n";
      }
  }

// Store table in work array.
// Construct extrema computation work-tables.
   $wArray = preg_split("[\n]", trim($APTable));
   $wCount = count($wArray);

   $EventsCount = $wCount / 5;

   $EventsString = $APSymbol = '';

   $w = $v = $ExtremaTableTTString = '';

   for ($i=0;   $i < $EventsCount;   $i++)
{
   for ($j=0;   $j < 5;   $j++)
   {
   $k = $j + 5*$i;
   $w = $wArray[$k];
   $APSymbol = substr($w, -1);
   $APSymbol = ($APSymbol == 'A' or $APSymbol == 'P')? $APSymbol:'';
   $ExtremaTableTTString .= substr($w,0,9) . " " . substr($w,35,10) . " $APSymbol\n";
   }

}

// Compute extrema from the extrema table.
// Each extremum table consists of 2 lines before
// to 2 lines after the marked event line ending
// with the extremum symbol (A or P).
// 2457666.5 405958.588 A
   $wArray = preg_split("[\n]", trim($ExtremaTableTTString));

   $out = $wTable = $w = '';

   for ($EventNumber=1;   $EventNumber <= $EventsCount;   $EventNumber++)
  {
   $j = 5*($EventNumber - 1);

   $APSymbol = " " . substr(trim($wArray[$j+2]), -1);

   $wTable = '';

   for ($i=$j;   $i < $j+5;   $i++)
  {
   $CurrLine = trim(substr($wArray[$i], 0,20));
   $JDTT = substr($wArray[$i], 0,9);

   $DistKm = substr($wArray[$i], 10,10);
   $wTable .= "$JDTT $DistKm\n";
  }

   $JDTTEvent = Extremum_5($wTable);
   $xDistKm = Lagrange_interp($wTable, $JDTTEvent);
   $JDTTEvent = sprintf("%7.8f", $JDTTEvent);
   $xDistKm = sprintf("%6.3f", $xDistKm);

   $out .= $JDTTEvent . " $xDistKm$APSymbol\n";

  } // End of  for(N=1 ...)

   return trim($out);

} // End of  Compute_Lunar_Apogee_Perigee (...)



/*

   Constructs a local time events table for any table given
   in "JDTT Event info" format.

   All text following the initial JDTT is an event code or
   comment text.

$EventsTableString =
"
2457632.87794313 NM
2457640.99306322 FQ
2457648.29599287 FM
2457654.91471884 LQ
";

*/

   function Lunar_Events_JDTT_to_LT ($LunarEventsTableStr, $TZhhmm='+00:00', $dTFlag=FALSE)
{
   $dTFracDay = 0;

   $TZFracDay = HMS_to_Hours($TZhhmm) / 24;

   $wArray = preg_split("[\n]", trim($LunarEventsTableStr));
   $wLinesCount = count($wArray);

   $out = '';

   for ($i=0;   $i < $wLinesCount;   $i++)
  {
   $CurrLine = strip_wspace($wArray[$i]);

   list($JDTTEvent, $EventString) = preg_split("[ ]", $CurrLine);

   $EventString = trim($EventString);



// Perigee values in km will always be > 300000 and < 400000
   if ($EventString > 300000 and $EventString < 400000)
   {
    $DistKm = floatval($EventString);
    $DistMi = sprintf("%6.2f mi", $DistKm/1.609344);
    $DistKm = sprintf("%6.2f km", $DistKm);
    $EventString = "P $DistMi = $DistKm";
   }


// Apogee values in km will always be > 400000.
   if ($EventString > 400000)
   {
    $DistKm = floatval($EventString);
    $DistMi = sprintf("%6.2f mi", $DistKm/1.609344);
    $DistKm = sprintf("%6.2f km", $DistKm);
    $EventString = "A  $DistMi = $DistKm";
   }



// Compute and apply Delta T if indicated.
   if ($dTFlag === TRUE)
      {
       $dTSeconds = round(Delta_T($JDTTEvent), 0);
       $dTFracDay = $dTSeconds / 86400;
      }

   $JDEvent = $JDTTEvent - $dTFracDay;

   $EventDateString = JD_Z_to_DT ($JDEvent, $TZhhmm, 2);
   $out .= "$EventDateString $EventString  $JDTTEvent\n";
  }
   $w = trim($out);

// ----------------------------------------------
// Replace lunar quarter phase and apogee/perigee
// symbols with full event name text strings with
// padding spaces for neat tabular display.

   $w = str_replace(' NM', ' New Moon               Phase 0/360 ', $w);
   $w = str_replace(' FQ', ' First Quarter Moon     Phase 90    ', $w);
   $w = str_replace(' FM', ' Full Moon              Phase 180   ', $w);
   $w = str_replace(' LQ', ' Last Quarter Moon      Phase 270   ', $w);
   $w = str_replace(' A ', ' Apogee ',  $w);
   $w = str_replace(' P ', ' Perigee ', $w);

   return $w;

} // End of  Lunar_Events_JDTT_to_LT (...)


/*
   XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
   Generates a table of lunar phases derived from a
   pre-computed ephemeris table string.
*/

   function Daily_Lunar_Phases_Table ($LunarEphemTable)
{
   $w = trim($LunarEphemTable);

   $wArray = preg_split("[\n]", $w);
   $wCount = count($wArray);

   list($q, $yyyy, $MonthName) = preg_split("[ ]", strip_wspace($wArray[1]));

   $YearMonthName = "$yyyy $MonthName";
   $MonthNameYear = "$MonthName $yyyy";

   $w = '';

   for ($i=4;   $i < $wCount-1;   $i++)
  {
   $CurrLine = strip_wspace($wArray[$i]);
   list($JDTT, $RAHr, $DeclDeg,  $DistKm, $PhaseDeg, $mDay) = preg_split("[ ]", $CurrLine);
   $JDNum    = sprintf("%7.0f", floor($JDTT + 0.5));
   $RAHMS    = Hours_to_HMS($RAHr,3,'','h');
   $RAHr     = sprintf("%2.7f",  $RAHr);
   $DeclDMS  = Deg_to_DMS($DeclDeg, 2, '+','d');
   $DeclDeg  = sprintf("%+2.6f", $DeclDeg);
   $DistMi   = sprintf("%6.3f",  $DistKm/1.609344);
   $DistKm   = sprintf("%6.3f",  $DistKm);
   $PhaseDeg = sprintf("%3.5f",  $PhaseDeg);

// ------------------------------
// Compute DoW for tabulated JDTT
   $wDoWString = trim(substr("Sunday   Monday   Tuesday  WednesdayThursday Friday   Saturday", 9*((floor($JDTT + 0.5) + 1) % 7), 9));


// Define 'position at ... ' text.
   $CoordinatesAt = "Coordinates&nbsp;at&nbsp;" . ((substr($JDTT, -2) == '.5')? "00:00&nbsp;TT&nbsp;&nbsp;&nbsp;Beginning&nbsp;of&nbsp;Date":"12:00&nbsp;TT&nbsp;&nbsp;&nbsp;Midday");

   $PhaseImgName = "phase_" . sprintf("%03d", round($PhaseDeg,0)) . ".png";
   $ImgHTML = "<img src=\"mini_lunar_phases/$PhaseImgName\" alt=''>";
   $PhaseDeg = sprintf("%3.4f", $PhaseDeg);

   if (floatval($mDay) <> 0)
      {
       $w .= "\n<tr><td style='color:silver; background-color:#180000; font-size:15px; border:1px solid maroon; text-align:center;'>$YearMonthName $mDay<br>$wDoWString<br>JD&nbsp;Number&nbsp;&nbsp;$JDNum<br>JD&nbsp;TT&nbsp;&nbsp;&nbsp;&nbsp;$JDTT</td><td style='color:gray; background-color:#000022; font-size:90%; border:1px solid DarkBlue; text-align:center;'>$ImgHTML<br><br>$PhaseDeg&nbsp;&deg;</td><td style='color:white; background-color:#001400;font-size:15px; text-align:left; border:1px solid green;'>$CoordinatesAt<br><br>Right&nbsp;Ascension&nbsp=&nbsp;$RAHMS&nbsp;=&nbsp;$RAHr&nbsp;h<br>Declination&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;$DeclDMS&nbsp;= $DeclDeg&nbsp;&deg;<br>Distance&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;=&nbsp;$DistMi&nbsp;mi&nbsp;&nbsp;&nbsp;=&nbsp;$DistKm&nbsp;km</td></tr>";
      }
  }

$TableHeader =
"
<table width='700' style='color:silver; font-family:monospace; font-weight:bold; background-color:black; font-size:15px; border:1px solid #444444;' align='botom' width='700' cellpadding='6'>
<tr style='font-size:15px;'><td colspan='3' style='background-color:black; color:GreenYellow; text-align:center; border:1px solid #444444;'>For&nbsp;<span style='color:white;'>$MonthName,&nbsp;$yyyy</span>&nbsp;at&nbsp;<span style='color:white;'>00:00:00&nbsp;TT</span>&nbsp;and&nbsp;<span style='color:white;'>12:00:00&nbsp;TT</span>&nbsp;on&nbsp;Each&nbsp;Date<br><br><span style='color:cyan;'>Geocentric&nbsp;Lunar&nbsp;Coordinates&nbsp;and&nbsp;Phase&nbsp;Table</span></td></tr>
<tr style='font-size:14px;'><td style='color:#FF4444; background-color:black; text-align:center; border:1px solid #444444;'>Calendar&nbsp;Date&nbsp;TT</td><td style='color:yellow; background-color:black; text-align:center; border:1px solid #444444;'>Lunar&nbsp;Phase</td><td style='color:GreenYellow; background-color:black; text-align:center; border:1px solid #444444;'>Geocentric&nbsp;Coordinates&nbsp;and&nbsp;Distance&nbsp;</td></tr>
";

$TableFooter =
"
<tr style='font-size:15px;'><td style='color:#FF4444; background-color:black; text-align:center; border:1px solid #444444;'>Calendar&nbsp;Date&nbsp;TT</td><td style='color:yellow; background-color:black; text-align:center; border:1px solid #444444;'>Lunar&nbsp;Phase</td><td style='color:GreenYellow; background-color:black; text-align:center; border:1px solid #444444;'>Geocentric&nbsp;Coordinates&nbsp;and&nbsp;Distance&nbsp;</td></tr>
<tr style='font-size:15px;'><td colspan='3' style='background-color:black; color:GreenYellow; text-align:center; border:1px solid #444444;'>For&nbsp;<span style='color:white;'>$MonthName,&nbsp;$yyyy</span>&nbsp;at&nbsp;<span style='color:white;'>00:00:00&nbsp;TT</span>&nbsp;and&nbsp;<span style='color:white;'>12:00:00&nbsp;TT</span>&nbsp;on&nbsp;Each&nbsp;Date<br><br><span style='color:cyan;'>Geocentric&nbsp;Lunar&nbsp;Coordinates&nbsp;and&nbsp;Phase&nbsp;Table</span></td></tr>
</table>
";

  return trim("$TableHeader\n$w$TableFooter");

} // End of  Daily_Lunar_Phases_Table (...)








/*
   This function is designed to analyze a yearly lunar ephemeris table
   and generate a corresponding report.

   All apogees and intervals between and min/max apogee for year
   All perigees and intervals between and min/max perigee for year
   All lunar quarter phases and intervals between

*/

   function Analyze_Yearly_Lunar_Table ($YearlyLunarEphemTable)
{

   global $_BAR_, $_Bar_, $bar_, $TZhhmm, $TimeMode;

// Read and store yearly table in working array.
   $w = preg_split("[\n]", trim($YearlyLunarEphemTable));
   $wCount = count($w);

// Initialize counts.
   $ApogeeCount = $PerigeeCount = 0;
   $NMCount = $FQCount = $FMCount = $LQCount = $MultiCount = 0;
   $BlueMoonCount = $BlackMoonCount = 0;

// Define distance extremes.
   $MinApogee = $MaxApogee = $MinPerigee = $MaxPerigee = 0;

// Initialize event tables.
   $ApogeeTable = $PerigeeTable = $sBlue = $sBlack = '';
   $NMTable = $FQTable = $FMTable = $LQTable = '';

   $BlueMoonTable = $BlackMoonTable = '';

// Count events.
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = trim($w[$i]);

   if (strpos($CurrLine, " Apogee  ") <> FALSE)
  {
   $CurrLine = str_rotate("$CurrLine  ", -33);
   $ApogeeTable .= "$CurrLine\n";
   $ApogeeCount++;
  }
   if (strpos($CurrLine, " Perigee ") <> FALSE)
  {
   $CurrLine = str_rotate("$CurrLine  ", -33);
   $PerigeeTable .= "$CurrLine\n";
   $PerigeeCount++;
  }
   if (strpos($CurrLine, " Phase 0/360 ") <> FALSE)
  {
   $NMTable .= "$CurrLine\n";
   $NMCount++;
  }
   if (strpos($CurrLine, " Phase 90 ") <> FALSE)
  {
   $FQTable .= "$CurrLine\n";
   $FQCount++;
  }
   if (strpos($CurrLine, " Phase 180 ") <> FALSE)
  {
   $FMTable .= "$CurrLine\n";
   $FMCount++;
  }
   if (strpos($CurrLine, " Phase 270 ") <> FALSE)
  {
   $LQTable .= "$CurrLine\n";
   $LQCount++;
  }

   if (strpos($CurrLine, "'blue moon'") <> FALSE)
  {
   $BlueMoonTable .= "$CurrLine\n";
   $BlueMoonCount++;
  }

   if (strpos($CurrLine, "'black moon'") <> FALSE)
  {
   $BlackMoonTable .= "$CurrLine\n";
   $BlackMoonCount++;
  }

  }

// Apply special cosmetic filters.
   $NMTable = str_replace("New Moon               Phase 0/360  ", "", $NMTable);
   $FQTable = str_replace("First Quarter Moon     Phase 90     ", "", $FQTable);
   $FMTable = str_replace("Full Moon              Phase 180    ", "", $FMTable);
   $LQTable = str_replace("Last Quarter Moon      Phase 270    ", "", $LQTable);

// Construct a apogees and perigees table
// sorted in ascending order of distance.
   $ApogeeTable  = str_replace(" Apogee ",   " ", $ApogeeTable);
   $PerigeeTable = str_replace(" Perigee ", "  ", $PerigeeTable);

// Construct apogee/perigee tables sorted by ascending distance.
   $ApogeeTableSorted  = Sort_Table($ApogeeTable);
   $PerigeeTableSorted = Sort_Table($PerigeeTable);

// Lunar quarter phases, apogees and perigees count totals.
   $QPhasesCount = $NMCount + $FQCount + $FMCount + $LQCount;
   $APCount = $ApogeeCount + $PerigeeCount;



/*
  Apogee/Perigee Distance     Julian Date TT       Date and Time LT      Event
---------------------------  ----------------  ------------------------ -------
225705.94 mi = 363238.49 km  2457763.75143133  2017 Jan 10 Tue 01:00:54 Perigee
*/










// Get year value from beginning of table.
// Should be the same for all tables.
   $Y = intval($NMTable);

   $FinalReport =
"$_BAR_
TABULAR SUMMARY FOR THE YEAR $Y $TimeMode

Time Mode:  $TimeMode
Time Zone:  UT$TZhhmm

$_BAR_
All $NMCount new moons of $Y $TimeMode
Time Zone   UT$TZhhmm
Black Moons $BlackMoonCount

     Date and Time $TimeMode      Julian Date TT
------------------------  ----------------
$NMTable

$_BAR_
All $FQCount first quarter moons of $Y $TimeMode
Time Zone UT$TZhhmm

     Date and Time $TimeMode      Julian Date TT
------------------------  ----------------
$FQTable

$_BAR_
All $FMCount full moons of $Y $TimeMode
Time Zone  UT$TZhhmm
Blue Moons $BlueMoonCount

     Date and Time $TimeMode      Julian Date TT
------------------------  ----------------
$FMTable

$_BAR_
All $LQCount last quarter moons of $Y $TimeMode
Time Zone UT$TZhhmm

     Date and Time $TimeMode      Julian Date TT
------------------------  ----------------
$LQTable

$_BAR_
All $ApogeeCount lunar apogees of $Y $TimeMode - Listed in Sequential Order
Time Zone UT$TZhhmm

      Apogee Distance         Julian Date TT       Date and Time $TimeMode
---------------------------  ----------------  ------------------------
$ApogeeTable
-----------------------------------------------------------------
All $ApogeeCount lunar apogees of $Y $TimeMode - Sorted by Distance (Min to Max)
Time Zone UT$TZhhmm

      Apogee Distance         Julian Date TT       Date and Time $TimeMode
---------------------------  ----------------  ------------------------
$ApogeeTableSorted

$_BAR_
All $ApogeeCount lunar perigees of $Y $TimeMode - Listed in Sequential Order
Time Zone UT$TZhhmm

      Perigee Distance        Julian Date TT       Date and Time $TimeMode
---------------------------  ----------------  ------------------------
$PerigeeTable

------------------------------------------------------------------
All $PerigeeCount lunar perigees of $Y $TimeMode - Sorted by Distance (Min to Max)
Time Zone UT$TZhhmm

      Perigee Distance        Julian Date TT       Date and Time $TimeMode
---------------------------  ----------------  ------------------------
$PerigeeTableSorted

$_BAR_";


// ------------------------------------------------------------
// Create a sequential table of all quarter phases in the year.

   $QPhasesTable =
"All $QPhasesCount Lunar Quarter Phases of $Y $TimeMode
Time Zone UT$TZhhmm

    Date and Time $TimeMode         Quarter Phase     Julian Date TT
------------------------  ------------------  ----------------
";

   $wArray = preg_split("[\n]", trim($YearlyLunarEphemTable));
   $wCount = count($wArray);

   for($i=0;   $i < $wCount;   $i++)
  {
    $CurrLine = trim($wArray[$i]);
    if
   (
    strpos($CurrLine, "New Moon") !== FALSE
    or
    strpos($CurrLine, "First Quarter Moon") !== FALSE
    or
    strpos($CurrLine, "Full Moon") !== FALSE
    or
    strpos($CurrLine, "Last Quarter Moon") !== FALSE
   )

   {
    $CurrLine = substr($CurrLine, 0, 24)."  ".substr($CurrLine, 25, 20).substr($CurrLine, -16);
    $QPhasesTable .= "$CurrLine\n";
   }
  }
    $QPhasesTable = RTrim($QPhasesTable);




// Create a sequential table of all apogees and perigees in the year.
   $APTable = '';

   $wArray = preg_split("[\n]", trim($YearlyLunarEphemTable));
   $wCount = count($wArray);
   for($i=0;   $i < $wCount;   $i++)
   {
   $CurrLine = trim($wArray[$i]);
   $CurrLine = str_rotate("$CurrLine  ", -33);
   if(strpos($CurrLine, "mi = ") <> FALSE) {$APTable .= "$CurrLine\n";}
   }

   $APTable = trim($APTable);

//   $APTable = Rotate_Text_Table ($APTable, -29, $EndSpace='');


/*
2017 Jan 05 Thu 14:46:58  First Quarter Moon  2457759.32509244
2017 Jan 12 Thu 06:33:57  Full Moon           2457765.98271458
2017 Jan 19 Thu 17:13:25  Last Quarter Moon   2457773.42678968
2017 Jan 27 Fri 19:07:00  New Moon            2457781.50567602
2017 Feb 03 Fri 23:18:51  First Quarter Moon  2457788.68056723
2017 Feb 10 Fri 19:32:52  Full Moon           2457795.52362954
2017 Feb 18 Sat 14:33:06  Last Quarter Moon   2457803.31546263
2017 Feb 26 Sun 09:58:21  New Moon            2457811.12466941
2017 Mar 05 Sun 06:32:20  First Quarter Moon  2457817.98160145
*/

// Rotate AP table so JDTT column comes first.
   $wArray = preg_split("[\n]", trim($APTable));
   $wCount = count($wArray);
   $APTable = '';
   for($i=0;   $i < $wCount;   $i++)
   {
   $CurrLine = trim($wArray[$i]);
   $CurrLine = str_rotate("$CurrLine ", -29);
   if(floatval($CurrLine) > 0) {$APTable .= "$CurrLine \n";}
   }
   $APTable = RTrim($APTable);

   $APTable = Str_Replace('Apogee',' Apogee ',  $APTable);
   $APTable = Str_Replace('Perigee',' Perigee', $APTable);

$APHeader =
"$_BAR_\nAll $ApogeeCount Apogees and $PerigeeCount Perigees of $Y $TimeMode - In Sequential Order
Time Zone UT$TZhhmm

 Julian Date TT        Date and Time $TimeMode      Event    Apogee/Perigee Distance
----------------  ------------------------  ------- ---------------------------";

   return RTrim($FinalReport) . "\n\n$QPhasesTable\n\n$APHeader\n$APTable";

} // End of  Analyze_Yearly_Lunar_Table (...)






// Compute hourly table for date starting at JDTT
// value corresponding to 00:00:00 LT of date.
//
// This table requires the LETable for the month
// from which it extracts the statistics for any
// given JDTT within the month-table range.
//
// Mode 1 = Basic 00 to 24-hour table.
// Mode 2 = Same table with 2 extra padding
// hours at each end.
//


   function Hourly_Lunar_Ephemeris_Table ($JDTTOrigin, $LETableStr, $Mode=1)
{
   $k = 2*(((intval($Mode) < 2)? 1:2)-1);

   $HourlyTableStr = '';

   for($hour = -$k;   $hour <= 24+$k;   $hour++)
{
// Compute current JDTT for hh hours offset from TT origin.
   $JDhhTT = $JDTTOrigin + $hour/24;

// Compute lunar ephem stats for current JD offset from origin.
   $LunarStatsStr = Lunar_Ephem_Stats ($JDhhTT, $LETableStr);

// Fetch values of the returned statistics.
   list
  (
   $JDhhTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg
  ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for neatness.
   $JDhhTT   = sprintf("%7.8f",  $JDhhTT);
   $hh       = sprintf("%02d",   $hour);
   $RAHrs    = substr("          ".sprintf("%1.7f",  $RAHrs),   -10);
   $DeclDeg  = substr("          ".sprintf("%+1.6f", $DeclDeg), -10);
   $DistMi   = substr("          ".sprintf("%6.3f",  $DistKm/1.609344),  -10);
   $DistKm   = substr("          ".sprintf("%6.3f",  $DistKm),  -10);
   $PhaseDeg = substr("          ".sprintf("%1.5f", $PhaseDeg), -8);

// Append current formatted ephemeris line to hourly table.
   $HourlyTableStr .= "$hh  $RAHrs  $DeclDeg  $DistKm  $DistMi  $PhaseDeg\n";
}

// Done
   return RTrim($HourlyTableStr );

   } // End of  Hourly_Lunar_Ephemeris_Table (...)




// Compute hourly lunar distance table for date, starting
// at JDTT corresponding to 00:00:00 LT of date.
// Updated to add 2 extra padding hours at each end of the
// table to facilitate interpolations near the ends of the
// hour table.

   function Hourly_Lunar_Distance_Table ($JDTTOrigin, $LETableStr)
{
   $HourlyDistanceTableStr = '';
   $PrevDistKm = 0;

   for($hour = -2;   $hour <= 24+2;   $hour++)
{
   $JDTThh = $JDTTOrigin + $hour/24;

   $LunarStatsStr = Lunar_Ephem_Stats ($JDTThh, $LETableStr);

// Fetch values of the listed statistics.
   list
      (
       $JDTThh,
       $q,
       $q,
       $DistKm,
       $q
      ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for neatness.
   $JDTThh   = sprintf("%7.8f", $JDTThh);
   $hh       = sprintf("%02d",  $hour);
   $DistMi   = sprintf("%6.3f", $DistKm/1.609344);
   $DistKm   = sprintf("%6.3f", $DistKm);

// Append current formatted distance ephemeris line to hourly table.
   $HourlyDistanceTableStr .= "$hh  $DistKm   $DistMi\n";

}

// Reconstruct hourly distance table with differences between lines.

// Store hourly distance table in working array.
   $wArray = preg_split("[\n]", RTrim($HourlyDistanceTableStr));
   $wCount = count($wArray);

   $out = '';

   for($hour=0;   $hour <= 24+4;   $hour++)
  {
   $CurrLine   = RTrim($wArray[$hour]);
   $CurrDistKm = substr($CurrLine, 3,11);
   $CurrDistMi = substr($CurrLine, 15,11);

   $hh = sprintf("%02d", $hour);

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

// Cut off extra end-line
   $out = RTrim($out);

// Done
   return RTrim($out);

   } // End of  Hourly_Lunar_Distance_Table (...)






// EXPERIMENTAL FUNCTIONS TO LOCATE THE QUARTER PHASES
// AND APOGEES AND PERIGEES WITHIN A PRECALCULATED
// 24-HOUR TABLE.  THE HOURLY TABLE ARGUMENT IS RAW
// MODE 2 TABLE WITHOUT HEADERS ($HourlyTableStr2).

   function Hourly_Q_Phase ($HourlyTableString)
{
   $out = $NMhhmmss = $FQhhmmss = $FMhhmmss = $LQhhmmss = '';
   $QP = '';

   $wTableStr = RTrim($HourlyTableString);
   $wArray = preg_split("[\n]", $wTableStr);
   $wCount = count($wArray);



// Check for new moon and construct interpolation table
// centered on first line where PhaseDeg value >= 360
   $u = floatval(substr(RTrim($wArray[2]),  -8));
   $w = floatval(substr(RTrim($wArray[26]), -8));
   $diff = $w - $u;

   if ($diff < 0)
  {
// Find 360/0 transition line index (i)
   for($i=2;   $i < 26;   $i++)
  {
   $u = floatval(substr(trim($wArray[$i-1]), -8));
   $w = floatval(substr(trim($wArray[$i-0]), -8));
   $diff = $w - $u;
   if ($diff < 0) {break;}
  }

// Construct 5-point interpolation table
// with adjustment for 360/0 transition.
   $out = '';
   for($j = $i-2;   $j < $i+3;   $j++)
  {
   $w  = RTrim($wArray[$j]);
   $hh = substr($w, 0, 2);
   $PhaseDeg = floatval(trim(substr($w, -8)));
   $PhaseDeg += ($j > 10)? 360:0;
   $out .= "$PhaseDeg  $hh\n";
  }
   $w = Lagrange_Interp($out, 360);
   $QP = Hours_to_HMS($w, 0, '', ':')."    New Moon = Phase 0&deg;";
  }



// Check for first quarter moon and construct interpolation
// table centered on first line where PhaseDeg value >= 90
   $u = floatval(substr(trim($wArray[2]),  -8));
   $w = floatval(substr(trim($wArray[26]), -8));
   if ($u < 90 and $w >= 90)
{
    for ($i=2;   $i <= 24;   $i++)
   {
    $PhaseDeg = substr(trim($wArray[$i]), -8);
    if ($PhaseDeg >= 90){break;}
   }
    for($j = $i-2;   $j < $i+3;   $j++)
   {
    $hh = substr(trim($wArray[$j]), 0,2);
    $PhaseDeg = floatval(substr(trim($wArray[$j]), -8));
    $out .= "$PhaseDeg  $hh\n";
   }
    $QP = hours_to_hms(Lagrange_interp($out, 90), 0, '', ':') ."    First Quarter Moon = Phase 90&deg;";
} // End of  if(...) FQ




// Check for full moon and construct interpolation table
// centered on first line where PhaseDeg value >= 180
   $u = floatval(substr(trim($wArray[2]),  -8));
   $w = floatval(substr(trim($wArray[26]), -8));
   if ($u < 180 and $w >= 180)
{
    for ($i=2;   $i <= 24;   $i++)
   {
    $PhaseDeg = substr(strip_wspace($wArray[$i]), -8);
    if ($PhaseDeg >= 180){break;}
   }
    for($j = $i-2;   $j < $i+3;   $j++)
   {
    $hh = substr(strip_wspace($wArray[$j]), 0,2);
    $PhaseDeg = floatval(substr(strip_wspace($wArray[$j]), -8));
    $out .= "$PhaseDeg  $hh\n";
   }
    $QP = hours_to_hms(Lagrange_interp($out, 180), 0, '', ':')."    Full Moon = Phase 180&deg;";
} // End of  if(...) FM



// Check for last quarter moon and construct interpolation
// table centered on first line where PhaseDeg value >= 270
   $u = floatval(substr(trim($wArray[2]),  -8));
   $w = floatval(substr(trim($wArray[26]), -8));
   if ($u < 270 and $w >= 270)
{
    for ($i=2;   $i <= 24;   $i++)
   {
    $PhaseDeg = substr(trim($wArray[$i]), -8);
    if ($PhaseDeg >= 270){break;}
   }
    for($j = $i-2;   $j < $i+3;   $j++)
   {
    $hh = substr(trim($wArray[$j]), 0,2);
    $PhaseDeg = floatval(substr(trim($wArray[$j]), -8));
    $out .= "$PhaseDeg  $hh\n";
   }
    $QP = hours_to_hms(Lagrange_interp($out, 270), 0, '', ':') . "    Last Quarter Moon = Phase 270&deg;";
} // End of  if(...) LQ




   return $QP;

} // End of  Hourly_Q_Phase(...)



/*
  Determine if or not there is an apogee on the date.

  This function assumes that the event is valid and exists
  within the 24-hour span of the date.

  If found, then compute it.

  If event is not found, then return an empty string.

  NOTE
  This function requires a Mode 2 hourly ephemeris table.

*/

   function Hourly_Apogee ($HourlyTableString)
{
// Determine if or not there is an apogee or perigee on the date.

   $wTableStr = RTrim($HourlyTableString);

   $wArray = preg_split("[\n]", $wTableStr);
   $wCount = count($wArray);

// Check if a possible apogee exists within table.
   list($q, $q,$q, $DistKm1) = preg_split("[ ]", strip_wspace($wArray[2]));
   list($q, $q,$q, $DistKm2) = preg_split("[ ]", strip_wspace($wArray[3]));
   $diff = $DistKm2 - $DistKm1;
   if ($diff < 0) {return '';}

// Check for apogee.
   $uTableStr = '';
   $k=0;

   for($i=3;   $i < 26;   $i++)
  {
   list($h01, $q,$q, $DistKm01) = preg_split("[ ]", strip_wspace($wArray[$i-1]));
   list($h02, $q,$q, $DistKm02) = preg_split("[ ]", strip_wspace($wArray[$i-0]));
   $diff = $DistKm02 - $DistKm01;
   if ($diff < 0) {$k=1;  break;}
  }

   if ($k == 1)
  {
// Construct working table (hh vs PhaseDeg).
   for($j = $i-2;   $j < $i+3;   $j++)
   {
   list($uhh, $q,$q, $uDistKm) = preg_split("[ ]", strip_wspace($wArray[$j]));

   $uhh = sprintf("%02d", intval($uhh));
   $uDistKm = sprintf("%6.3f", $uDistKm);
   $uTableStr .= "$uhh  $uDistKm\n";
   }
// Compute time (hh) of apogee within date.
   $hhApogee = Extremum_5($uTableStr);
   $hhmmssApogee = Hours_to_HMS($hhApogee, 0, '', ':');

// Compute perigee distance in km and mi
   $ApogeeDistKm = Lagrange_Interp($uTableStr, $hhApogee);
   $ApogeeDistMi = sprintf("%6.3f", $ApogeeDistKm/1.609344);
   $ApogeeDistKm = sprintf("%6.3f", $ApogeeDistKm);

   $w = trim("$hhmmssApogee    Apogee  at $ApogeeDistKm km = $ApogeeDistMi mi");

   if (substr($w, 0, 1) == '-') {return '';}
   if (substr($w, 0, 1) == '3') {return '';}

   return $w;
  }

}









/*
  Determine if or not there is an perigee on the date.

  This function assumes that the event is valid and exists within
  the 24-hour span of the date.

  If found, then compute it.

  If the event is not found, then it returns an empty string.

  NOTE
  This function requires a Mode 2 hourly ephemeris table.

*/

   function Hourly_Perigee ($HourlyTableString)
{
   $wTableStr = RTrim($HourlyTableString);

   $wArray = preg_split("[\n]", $wTableStr);
   $wCount = count($wArray);

// Check if a possible perigee exists within table.
   list($q,$q,$q, $DistKm1) = preg_split("[ ]", strip_wspace($wArray[2]));
   list($q,$q,$q, $DistKm2) = preg_split("[ ]", strip_wspace($wArray[3]));
   $diff = $DistKm2 - $DistKm1;
   if ($diff > 0) {return '';}

// Check for perigee.
   $uTableStr = '';
   $k=0;

   for($i=3;   $i < 26;   $i++)
  {
   list($h01, $q,$q, $DistKm01) = preg_split("[ ]", strip_wspace($wArray[$i-1]));
   list($h02, $q,$q, $DistKm02) = preg_split("[ ]", strip_wspace($wArray[$i-0]));
   $diff = $DistKm02 - $DistKm01;
   if ($diff > 0) {$k=1;  break;}
  }

   if ($k == 1)
  {
// Construct working table (hh vs PhaseDeg).
   for($j = $i-2;   $j < $i+3;   $j++)
   {
   list($uhh, $q,$q, $uDistKm) = preg_split("[ ]", strip_wspace($wArray[$j]));

   $uhh = sprintf("%02d", intval($uhh));
   $uDistKm = sprintf("%6.3f", $uDistKm);
   $uTableStr .= "$uhh  $uDistKm\n";
   }
// Compute time (hh) of perigee within date.
   $hhPerigee = Extremum_5($uTableStr);
   $hhmmssPerigee = Hours_to_HMS($hhPerigee, 0, '', ':');

// Compute perigee distance in km and mi
   $PerigeeDistKm = Lagrange_Interp($uTableStr, $hhPerigee);
   $PerigeeDistMi = sprintf("%6.3f", $PerigeeDistKm/1.609344);
   $PerigeeDistKm = sprintf("%6.3f", $PerigeeDistKm);

   $w = trim("$hhmmssPerigee    Perigee at $PerigeeDistKm km = $PerigeeDistMi mi");

   return $w;
  }

} // End of  Hourly_Perigee(...)




/*
   This function computes the distance between two points in space
   given the RA, Declination and Distance of each point as viewed
   from Earth.
*/

   function Dist_Point_A_to_Point_B ($ARAHrs,$ADeclDeg,$ADist, $BRAHrs,$BDeclDeg,$BDist)
{
   $ARADeg = 15*$ARAHrs;
   $Ax = $ADist * cosd($ARADeg) * cosd($ADeclDeg);
   $Ay = $ADist * sind($ARADeg) * cosd($ADeclDeg);
   $Az = $ADist * sind($ADeclDeg);

   $BRADeg = 15 * $BRAHrs;
   $Bx = $BDist * cosd($BRADeg) * cosd($BDeclDeg);
   $By = $BDist * sind($BRADeg) * cosd($BDeclDeg);
   $Bz = $BDist * sind($BDeclDeg);

   $x = $Bx - $Ax;
   $y = $By - $Ay;
   $z = $Bz - $Az;

// ----------------------------------------------------------
// COMPUTE AND RETURN DIRECT SPATIAL DISTANCE BETWEEN POINTS.

   return sqrt($x*$x + $y*$y + $z*$z);
}




























// Same as above but  with approaching/receding labels.
   function Hourly_Lunar_Dist_Diff_Table ($JDTTOrigin, $LETableStr, $Mode=1)
{
   $HourlyDistanceTableStr = '';
   $PrevDistKm = 0;

   $k = 2*(((intval($Mode) < 2)? 1:2)-1);

   for($hour = -$k;   $hour <= 24+$k;   $hour++)
{
   $JDTThh = $JDTTOrigin + $hour/24;

   $LunarStatsStr = Lunar_Ephem_Stats ($JDTThh, $LETableStr);

// Fetch values of the listed statistics.
   list
      (
       $JDTThh,
       $q,
       $q,
       $DistKm,
       $q
      ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for neatness.
   $JDTThh   = sprintf("%7.8f", $JDTThh);
   $hh       = sprintf("%02d",  $hour);
   $DistMi   = sprintf("%6.3f", $DistKm/1.609344);
   $DistKm   = sprintf("%6.3f", $DistKm);

// Append current formatted distance ephemeris line to hourly table.
   $HourlyDistanceTableStr .= "$hh  $DistKm  $DistMi\n";

}

// Reconstruct hourly distance table with differences between lines.

// Store hourly distance table in working array.
   $wArray = preg_split("[\n]", RTrim($HourlyDistanceTableStr));
   $wCount = count($wArray);

   $out = '';

   for($hour=0;   $hour <= 24+2*$k;   $hour++)
  {
   $CurrLine   = RTrim($wArray[$hour]);
   $NextLine   = @RTrim($wArray[$hour+1]);

   $CurrDistKm = substr($CurrLine, 3,11);
   $NextDistKm = substr($NextLine, 3,11);

   $CurrDistMi = substr($CurrLine, 15,11);
   $NextDistMi = substr($NextLine, 15,11);

   $DiffDistKm = $NextDistKm - $CurrDistKm;
   $DiffDistMi = $NextDistMi - $CurrDistMi;

   $DiffDistKm = sprintf("%+1.3f", $DiffDistKm);
   $DiffDistMi = sprintf("%+1.3f", $DiffDistMi);

   $hh = sprintf("%02d", $hour);

// Format difference values
   $DiffDistKm = substr("        $DiffDistKm", -8);
   $DiffDistMi = substr("        $DiffDistMi", -8);

   $AppRecStr = ($DiffDistKm < 0)? 'approaching':'receding';



   $out .= "$CurrLine\n      $DiffDistKm    $DiffDistMi  diff $AppRecStr   \n";
  }

// Cut off extra end-line
   $out = RTrim($out);

   $out = substr($out, 0, strlen($out)-42);

// Done
   return RTrim($out);

   } // End of  Hourly_Lunar_Dist_Diff_Table (...)




   function Hourly_Lunar_Dist_Diff_Table2 ($JDTTOrigin, $LETableStr, $Mode=1)
{
   $HourlyDistanceTableStr = '';
   $PrevDistKm = 0;

   $k = 2*(((intval($Mode) < 2)? 1:2)-1);

   for($hour = -$k;   $hour <= 24+$k;   $hour++)
{
   $JDTThh = $JDTTOrigin + $hour/24;

   $LunarStatsStr = Lunar_Ephem_Stats ($JDTThh, $LETableStr);

// Fetch values of the listed statistics.
   list
      (
       $JDTThh,
       $RAHrs,
       $DeclDeg,
       $DistKm,
       $q
      ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for neatness.
   $JDTThh   = sprintf("%7.8f", $JDTThh);
   $hh       = sprintf("%02d",  $hour);
   $DistMi   = sprintf("%6.3f", $DistKm/1.609344);
   $DistKm   = sprintf("%6.3f", $DistKm);

// Append current formatted distance ephemeris line to hourly table.
   $HourlyDistanceTableStr .= "$hh  $DistKm  $DistMi\n";

}

// Reconstruct hourly distance table with differences between lines.

// Store hourly distance table in working array.
   $wArray = preg_split("[\n]", RTrim($HourlyDistanceTableStr));
   $wCount = count($wArray);

   $out = '';

   for($hour=0;   $hour <= 24+2*$k;   $hour++)
  {
   $CurrLine   = RTrim($wArray[$hour]);
   $NextLine   = @RTrim($wArray[$hour+1]);

   $CurrDistKm = substr($CurrLine, 3,11);
   $NextDistKm = substr($NextLine, 3,11);

   $CurrDistMi = substr($CurrLine, 15,11);
   $NextDistMi = substr($NextLine, 15,11);

   $DiffDistKm = $NextDistKm - $CurrDistKm;
   $DiffDistMi = $NextDistMi - $CurrDistMi;

   $DiffDistKm = sprintf("%+1.3f", $DiffDistKm);
   $DiffDistMi = sprintf("%+1.3f", $DiffDistMi);

   $hh = sprintf("%02d", $hour);

// Format difference values
   $DiffDistKm = substr("        $DiffDistKm", -8);
   $DiffDistMi = substr("        $DiffDistMi", -8);

   $AppRecStr = ($DiffDistKm < 0)? 'approaching':'receding';

// COMPUTE ORBITAL SPEED FROM RA, DECL, DIST





   $out .= "$CurrLine\n      $DiffDistKm    $DiffDistMi  diff $AppRecStr   \n";
  }

// Cut off extra end-line
   $out = RTrim($out);

   $out = substr($out, 0, strlen($out)-42);

// Done
   return RTrim($out);

   } // End of  Hourly_Lunar_Dist_Diff_Table2 (...)










// Define status message to be displayed while working.
   $w = "               COMPUTING ... THIS WILL TAKE A FEW SECONDS.";
   $StatusText1 = "OutputText.innerHTML='$w';InsertMoonText.innerHTML='$w'";

   $InsertMoonText = '';

   $TX = "black";
   $BG = "white";

// -----------------------------------
// Get current year from system clock.
   $cYear = date("Y");

/* -----------------------------------------------
   Set the path to the program to be executed when
   any submit button is clicked.  In this case, it
   is this program itself. It modifies itself each
   time it is called.
*/
   $RunThisProgram = $_SERVER["SCRIPT_NAME"];

/* --------------------------
   Define titles or headings.
*/
  $_InterfaceTitle_ = "<span style='font-size:15px;'>DE405 Lunar Ephemeris Calculator</span><br><span style='font-size:10px; line-height:200%;'>Based on the 600-year NASA/JPL DE405 Ephemeris Model<br>Spanning the Period From 1600 to 2200 on the Modern Gregorian Calendar</span>";

   $_HTMLTitle_ = "DE405 Lunar Ephemeris Calculator";


/* ----------------------------------------------------
   Define a name for the cookie and set expiration date
   to 30 days if not used or deleted before then.  This
   value is expressed in seconds from the time that the
   cookie is created (Time Now + days*86400 seconds).
*/
   $CookieName = 'Date_Time_Location_Interface_Settings';

   $SetToExpireIn30Days = time() + 30*86400;

   $OutputText = $TimeMode = $dThhmmss = '';

   $TZFracDay = $dTFracDay = 0.0;


/* First check to see if a previous cookie exists.  If so
   then recall the previous interface values from it.  If
   not, then create a new cookie and save the interface
   settings in it.

   TRUE/FALSE = Cookie Found / Cookie Not found
*/
   $_COOKIE_FOUND_ = isset($_COOKIE[$CookieName]);

   if (!isset($_POST['ComputeButton']))
{

/* If there is a previous cookie, then restore
   the interface settings from it.
*/
   if ($_COOKIE_FOUND_)
  {
   $CookieDataString = $_COOKIE[$CookieName];
   list($yyyy,$mn,$dn,$hhmmss,$TimeMode,$TZhhmm,$latitude,$longitude,$elevation)
   = preg_split("[\|]", $CookieDataString);

   $hhmmss = Hours_to_HMS(HMS_to_Hours($hhmmss), 0, '',':');
   $TZhhmm = substr(Hours_to_HMS(HMS_to_Hours($TZhhmm), 0, '+',':'), 0,6);
   $TZFracDay = sprintf("%+1.8f", HMS_to_Hours($TZhhmm)/24);

// Compute Delta T resolved to nearest second.
// This is applied only if UT or LT mode.
   $dTSeconds = round(Delta_T(100*intval($yyyy) + intval($mn)), 0);
   $dThhmmss  = Hours_to_HMS($dTSeconds/3600, 0, '+', ':');
   $dTFracDay = sprintf("%+1.8f", $dTSeconds/86400);
  }

else

/* If there is no previous cookie, then set default
   interface values and store them in a new cookie.
*/
  {
   $yyyy      = date('Y');
   $mn        = date('m');
   $dn        = date('d');
   $hhmmss    = date('H:i:s');
   $TimeMode  = 'LT';
   $TZhhmm    = date('Z')/3600; // Local TZ automatically adjusted for DST

   $hhmmss    = Hours_to_HMS(HMS_to_Hours($hhmmss), 0, '',':');
   $TZhhmm    = substr(Hours_to_HMS(HMS_to_Hours($TZhhmm), 0, '+',':'), 0,6);
   $TZFracDay = sprintf("%+1.8f", HMS_to_Hours($TZhhmm)/24);

// Compute Delta T resolved to nearest second.
// This is applied only if UT or LT mode.
   $dTSeconds = round(Delta_T(100*intval($yyyy) + intval($mn)), 0);
   $dThhmmss  = Hours_to_HMS($dTSeconds/3600, 0, '+', ':');
   $dTFracDay = sprintf("%+1.8f", $dTSeconds/86400);

// Set the default location to home.
// Ovid NY, USA (coords From www.gps-coordinates.net)
     $latitude  = '+42.6764722222';
     $longitude = '-76.8230000000';

// Set the default location to Greenwich, UK.
//   $latitude  = '+51 28 40.1';
//   $longitude = '-00 00 05.4';

   $latitude  = DMS_to_Deg($latitude);
   $longitude = DMS_to_Deg($longitude);

   $LatDMS = Deg_to_DMS($latitude,  2, '+', 'd');
   $LonDMS = Deg_to_DMS($longitude, 2, '+', 'd');

   $elevation = '+0.0'; // meters

   $latitude  = sprintf('%+1.7f', $latitude);
   $longitude = sprintf('%+1.7f', $longitude);

   $CookieDataString = "$yyyy|$mn|$dn|$hhmmss|$TimeMode|$TZhhmm|$latitude|$longitude|$elevation";
   setcookie ($CookieName, $CookieDataString, $SetToExpireIn30Days);
  }
}


   if (isset($_POST['ComputeButton']))
{
   $yyyy     = @trim($_POST['yyyy']);
   $mn       = @trim($_POST['mn']);
   $dn       = @trim($_POST['dn']);
   $hhmmss   = @trim($_POST['hhmmss']);

// PATCH TO CONVERT NUMERICAL TIME STRING INTO NORMAL
// TIME STRING FORMAT WITH COLONS BETWEEN 2-DIGIT PAIRS.
   $hhmmss = Num_HMS_Trans ($hhmmss);

   $TimeMode = @trim($_POST['TimeMode']);
   $TimeMode = strtoupper(substr($TimeMode, 0,1));

// Set default TimeMode to UT mode if TimeMode code unrecognized.
   if ($TimeMode <>'L' and $TimeMode <> 'U' and $TimeMode <> 'T')
      {$TimeMode = 'UT';}
   else
      {$TimeMode = substr($TimeMode, 0,1).'T';}

// Force time zone value to zero if UT or TT mode.
   if ($TimeMode == 'UT' or $TimeMode == 'TT') {$TZhhmm = '+00:00';}

   $TZhhmm   = @trim($_POST['TZhhmm']);

   $latitude  = @trim($_POST['latitude']);
   $longitude = @trim($_POST['longitude']);

   $latitude  = DMS_to_Deg($latitude);
   $longitude = DMS_to_Deg($longitude);

   $LatDMS = Deg_to_DMS($latitude,  2, '+', 'd');
   $LonDMS = Deg_to_DMS($longitude, 2, '+', 'd');

   $elevation = @trim($_POST['elevation']);

// PATCH TO INTERNALLY CONVERT INPUT FEET TO METERS
// Default elevation assumes m.  For feet, attach ft to end of value.
// Then, if elevation is in feet, it is converted to the nearest meter.
   if(strpos(strtolower($elevation), 'ft') !== FALSE) {$elevation *= 0.3048;}
   $elevation = sprintf("%+1.1f", $elevation);


// Compute Delta T resolved to nearest second.
// This is applied only if UT or LT mode.
   $dTSeconds = round(Delta_T(100*intval($yyyy) + intval($mn)), 0);
   $dThhmmss  = Hours_to_HMS($dTSeconds/3600, 0, '+', ':');
   $dTFracDay = sprintf("%+1.8f", $dTSeconds/86400);


// Tidy up input values.
// Format latitude and longitude values.
   $latitude  = sprintf("%+1.7f", $latitude);
   $longitude = sprintf("%+1.7f", $longitude);

   $SN = ($latitude  < 0)? 'deg S':'deg N';  if ($latitude == 0) {$SN = ' ';}
   $WE = ($longitude < 0)? 'deg W':'deg E';  if ($longitude == 0){$WE = ' ';}

   $LonHrs = $longitude/15;
   $LonHMS = Hours_to_HMS ($LonHrs, 2, '', 'h');
   $LonHrs = sprintf("%+1.7f", $LonHrs);

   $mn = sprintf("%02d", intval($mn));
   $dn = sprintf("%02d", intval($dn));

   $hhmmss = Hours_to_HMS(HMS_to_Hours($hhmmss), 0, '',':');
   $TZhhmm = substr(Hours_to_HMS(HMS_to_Hours($TZhhmm), 0, '+',':'), 0,6);
   $TZFracDay = sprintf("%+1.8f", HMS_to_Hours($TZhhmm)/24);

// Format latitude and longitude values.
   $latitude  = sprintf("%+1.7f", $latitude);
   $longitude = sprintf("%+1.7f", $longitude);

   $CookieDataString = "$yyyy|$mn|$dn|$hhmmss|$TimeMode|$TZhhmm|$latitude|$longitude|$elevation";
   setcookie ($CookieName, $CookieDataString, $SetToExpireIn30Days);
}


// ---------------------------------
// Check if info button was clicked.
// If so, then display program info.

   $_INFO_BUTTON_CLICKED_ = FALSE;

   if (isset($_POST['InfoButton']))
{
   $_INFO_BUTTON_CLICKED_ = TRUE;

   $TextSize = '18px';

   $PhaseDeg = $PhaseDegText = '';

   $TX = "black";
   $BG = "white";

   $LunarPhaseIMG = "<img src='small_lunar_phases/blank_transp.png' alt=''><br><br>&nbsp;";

   $N = '';

   $OutputText =
"This is a basic high-precision moon calculator for any date, time, zone and
geographic location based on the 600-Year NASA/JPL DE405 ephemeris model.

Valid from 1600 to 2200 on the modern Gregorian calendar.

PHP 5 Astronomy Program Written by Jay Tanner

$_HR5_
This program computes the basic geocentric  and topocentric lunar ephemeris
statistics and displays the phase of the moon for any given Gregorian calen-
dar date over the 600-year period from 1600 to 2200.

It computes a table of all the basic lunar events for the given month.

The basic lunar events include:
New Moon
First Quarter Moon
Full Moon
Last Quarter Moon
Perigee - Minimum orbital distance
Apogee  - Maximum orbital distance

It also computes an hourly ephemeris table and a table of lunar radial and
orbital velocities.

The radial velocity is the velocity along the Earth-Moon radius vector.  It
tells how fast the moon is approaching or receding from the Earth.  A nega-
tive radial velocity means the moon is approaching the Earth and a positive
value means the moon is receding from the Earth.  At the lunar apogee and
perigee points, the radial velocity equates to zero, after which it
reverses sign.

The orbital velocity is the speed at which the moon is traveling around its
elliptical orbit, perpendicular to the radial velocity. On the average, the
moon moves through space at  approximately 1 lunar diameter per hour in its
orbit.  This slight orbital motion displacement can be detected against the
stars during one hour of close observation, if reference stars are visible.

$_HR5_
THEORETICAL ACCURACY OF THIS DE405 IMPLEMENTATION

------------------------------------------
VALUE                           TO NEAREST
-----------------------------   ----------
Time                            second TT
Lunar phase image displayed     degree
Right Ascension                 0.001s
Declination                     0.01&#34;
Distance mi                     0.001 mi
Distance km                     0.001 km

NOTE:
The rise/set times should only be taken to the nearest minute at best because
atmospheric refraction due to temperature, pressure and humidity can unpredict-
ably effect the actual rise/set times by as much as a few minutes either way.
Thus, the way the local weather conditions will effect the rise set times can
not be predicted very accurately, but rise/set times do not generally need to
be resolved to the second anyway. To the nearest minute or two is good enough
for all practical purposes when it comes to rise/set times.

$_HR5_
SYMBOLS:

LT  =  Local Time (hh:mm:ss) within the given Time Zone
TZ  =  Local Time Zone offset (&plusmn;hh:mm) from UT (Neg/Pos = West/East)
UT  =  Universal (standard) Time (also called GMT)
dT  =  Delta T resolved to nearest second (from NASA polynomial expressions)
TT  =  Terrestrial Time (also called TDB) used for astronomical computations

GST =  Greenwich Sidereal Time (True apparent)
LST =  Local Sidereal Time (True apparent) for given geographic location

Time Scale Relationships:
UT  =  LT &minus; TZ
TT  =  UT + dT
or
TT  =  LT - TZ + dT

$_HR5_
NOTES:

1:
The Date/Time mode setting overrides any Time Zone offset setting. Setting to
either UT or TT mode WILL AUTOMATICALLY RESET the Time Zone offset to +00:00,
which is necessary for it to work correctly or it would otherwise have to be
done manually anyway.

2:
The time zone offset setting applies ONLY to LT (Local Time) mode in the given
time zone offset locale and can only be changed to non-zero when in LT mode.

3:
The Delta T value is computed from the NASA polynomial expressions for Delta T,
resolved to the nearest second, and is applied ONLY in UT and LT modes.  This
value has the greatest uncertainty of the time elements and can only be esti-
mated for remote dates.

4:
The applied nutation is computed from the 1980 IAU series.

5:
Since this program is running on a remote web server, you may be in another
time zone, so the default settings will need to be adjusted according to
your locale. The default location is set to my home location, which was
used to test the program as it was being developed. This can easily be
changed to any other locale as needed.

6: IMPORTANT
The lunar phase image displayed is intended to approximate a view through
binoculars or a small telescope. To do this correctly, the given time zone
has to properly match the given geographic location.

For example, if the longitude is -76 degrees, we would use the time zone
offset of UT-05 hours.  If the time zone offset of UT+09 was given, then
the rising and setting times and the orientation of the lunar image would
not be correct for the given location because the given location is not
actually within the given time zone. The program simply assumes that the
correct values are entered and performs the computations accordingly and
does NOT check for invalid values since there is no way to reliably tell
if both are valid at the same time.  We either know the correct values to
use or we don&#39;t.

7:
The size of the displayed lunar phase image is scaled according to distance
so that when  the moon is at perigee, it will be largest and at apogee, the
image will be smallest,  with distances in-between scaled relatively.  This
small degree of virtual size-vs-distance scaling is very subtle and may not
even be noticed.

8:
A COOKIE IS USED to remember the interface date/time/locale settings. If you
navigate to another page and return at another time, the interface settings
will be recalled, unless the cookie was deleted or more than 30 days have
passed since the last refresh.  The cookie is set to expire automatically
in 30 days if not refreshed or deleted before then. It does not track or
count page visits, it only stores the given interface settings used for
the computations and nothing more, for the convenience of the user, and
holds them for up to 30 days at most.  IT IS A HARMLESS LOCAL COOKIE
and Internet privacy is NOT compromised in any way.
";
}


/* --------------------------------------
   Filter for interface date input error.
*/
   $_DATE_ERROR_ = FALSE;

   $Y = intval($yyyy);
   $m = intval($mn);
   $d = intval($dn);

   $_DATE_ERROR_ = (!CheckDate($m, $d, $Y) or $Y < 1600 or $Y > 2200);

   if ($_DATE_ERROR_ and !$_INFO_BUTTON_CLICKED_)
  {
   $PhaseDeg = $PhaseDegText = '';
   $LunarPhaseIMG = "<img src='small_lunar_phases/blank_transp.png' alt=''><br><br>";

   $TX = "white";
   $BG = "maroon";

   $OutputText =
"DATE ERROR:

The given date elements (Year/Month/Day): $yyyy/$mn/$dn
do not resolve to a valid date on the actual calendar or
the year is outside of the valid range from 1600 to 2200.

---------------------
VALID DATE ARGUMENTS:

Year Number  : From 1600 to 2200 on modern Gregorian calendar

Month Number : From 1 to 12 (1=Jan, 2=Feb, 3=Mar, ..., 12=Dec)

Day Number   : From 1 to 31, depending on the given month and
               whether or not the given year is a leap year.
";
  }














/* ============================================
   COMPUTE TIME VARIABLES USED FOR COMPUTATIONS
   IF OUTPUT TEXT BLOCK IS STILL EMPTY.
*/

  if (trim($OutputText) == '')
{
  $yyyymndn = 10000*$Y + 100*$m + $d;

  $JD = DTZ_to_JD("$yyyymndn $hhmmss");

  if ($TimeMode == 'UT')
 {
  $TZhhmm = '+00:00';
  $JDUT = $JD;
  $JDTT = $JDUT + $dTFracDay;
  $JDLT = $JDUT;
 }

  if ($TimeMode == 'LT')
 {
  $JDLT = $JD;
  $JDUT = $JDLT - $TZFracDay;
  $JDTT = $JDUT + $dTFracDay;
 }

  if ($TimeMode == 'TT')
 {
  $TZhhmm = '+00:00';
  $JDTT = $JD;
  $JDUT = $JDTT - $dTFracDay;
  $JDLT = $JDTT;
 }



// FOR MAKING A TABLE ANCHORED AT 00H LT OF DATE:
// Compute JDTT table origin value corresponding
// to local time = 00:00:00 of date
   $JDxx = GregorianToJD(intval($mn), intval($dn), intval($yyyy));
   $JDTTOrigin = floor($JDxx + 0.5) - 0.5 - $TZFracDay + (($TimeMode == 'TT')? 0:$dTFracDay);
   $JDTTOrigin = sprintf("%7.8f", $JDTTOrigin);

// Compute JD number TT
   $JDNumTT = floor($JDTT + 0.5);

// Compute extended lunar ephemeris table for given year/month argument.
// This ephemeris table spans 1 month at 12-hour intervals. It
// is used as a source to make smaller tables for daily use.
   $LETableStr = Lunar_Ephemeris_TableX ("$yyyy$mn");


// Compute basic lunar statistics for given JDTT within table.
// These statistics apply to the current date/time/zone settings.
   $LunarStatsStr = Lunar_Ephem_Stats ($JDTT, $LETableStr);

// Parse individual lunar stats values.
   list
  (
   $wJDTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg
  ) = preg_split("[ ]", strip_wspace($LunarStatsStr));




// Compute basic lunar ephemeris statistics for JDTT within month table.
   $LunarEphemStats = LE_Stats($JDTT, $LETableStr, $latitude, $longitude, $elevation);

// Parse and extract all the basic lunar ephemeris statistics.
// All subsequent computations will be based on these statistics.
   list
  (
   $wJDTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg,
   $AngDiamDeg,
   $IllumPercent,
   $LHADeg,
   $ParallAngDeg,
   $HPDeg,
   $TopoRAHrs,
   $TopoDeclDeg,
   $TopoAzimDegN0,
   $AzimDirSymbol,
   $TopoAltDeg
  ) = preg_split("[ ]", strip_wspace($LunarEphemStats));


// Derive auxiliary values from above values.
   $RADeg     = 15 * $RAHrs;
   $TopoRADeg = 15 * $TopoRAHrs;
   $LHAHrs    = $LHADeg / 15;

// Compute alternate azimuth for South = 0 degrees.
   $TopoAzimDegS0 = $TopoAzimDegN0 + 180;
   $TopoAzimDegS0 -= (($TopoAzimDegS0 > 360)? 360:0);

// Paralactic angle by which to rotate lunar image clockwise
// This is used only to rotate the lunar phase image in HTML
// via CSS.
   $ParallAngRot = $ParallAngDeg . 'deg';


// Compute surface-to-surface light-time.
// Same as minimum ideal radar distance.
   $LTSeconds = ($DistKm - 8104.4943) / 299792.458;
   $LTSec = sprintf("%1.4f", $LTSeconds);
   $LT2xSeconds = sprintf("%1.4f", 2*($DistKm - 8104.4943) / 299792.458);
   $LT2xSec = sprintf("%1.4f", $LT2xSeconds);



// -----------------------------------------------------
// FORMAT SOME OF THE ABOVE LUNAR STATISTICS FOR DISPLAY
   $LHAHMS = Hours_to_HMS($LHAHrs, 2, '+', 'h');
   $LHADeg = sprintf("%+3.7f", $LHADeg);
   $LHAHrs = sprintf("%+2.7f", $LHAHrs);

   $TopoAzimDegS0 = sprintf("%3.3f", $TopoAzimDegS0);

   $HPDMS = Deg_to_DMS($HPDeg, 2);
   $HPMM  = sprintf("%1.5f", $HPDeg*60);
   $HPSS  = sprintf("%6.2f", $HPDeg*3600);
   $HPDeg = sprintf("%1.7f", $HPDeg);
   $HPDMS = str_replace("00&deg; ", "", $HPDMS);

   $AngDiamDMS = deg_to_DMS($AngDiamDeg, 2, '', 'd');
   $AngDiamDMS = str_replace("00&deg; ", "", $AngDiamDMS);
   $AngDiamMM  = sprintf("%1.5f", $AngDiamDeg*60);
   $AngDiamSS  = sprintf("%1.2f", $AngDiamDeg*3600);
   $AngDiamStr = $AngDiamDMS;

   $TopoRAHMS   = Hours_to_HMS($TopoRAHrs, 3, '',  'h');
   $TopoDeclDMS = Deg_to_DMS($TopoDeclDeg, 2, '+', 'd');
   $TopoRAHrs   = sprintf("%2.7f",   $TopoRAHrs);
   $TopoRADeg   = sprintf("%2.7f",   $TopoRADeg);
   $TopoDeclDeg = sprintf("%+2.6f",  $TopoDeclDeg);
   $TopoAltDeg3  = sprintf("%+2.3f", $TopoAltDeg);





// ------------------------------------------------------------
// Determine if moon is ascending (between rising and meridian)
// or descending (between meridian and setting).  If below the
// horizon, then invisible from the given location.

// Determine if rising or setting.
// Rising if ascending and alt > hDeg and alt < 1.75
// Setting if descending and alt > hDeg and alt < 1.75
// This is to account for the average refraction on the horizon.

// Compute (h) accounting for variable
// angular diameter and parallax:
   $hDeg = -0.5833 - $HPDeg; // (h) for center of moon on horizon.

   $AscenDecenRiseSet = (($ParallAngDeg < 0)? "A":"De") . "scending";

   if($AscenDecenRiseSet == "Ascending" and $TopoAltDeg > $hDeg and $TopoAltDeg < 1.75)
     {$AscenDecenRiseSet = "RISING";}

   if($AscenDecenRiseSet == "Descending" and $TopoAltDeg > $hDeg and $TopoAltDeg < 1.75)
     {$AscenDecenRiseSet = "SETTING";}

   $ABHorizon = (($TopoAltDeg < -$hDeg)? 'BELOW':'ABOVE') . ' local horizon';

   $ABHorizon .= " - $AscenDecenRiseSet";















// Get phase to nearest degree as 3-digit integer string.
   $PhaseXXX = sprintf("%03d", floor($PhaseDeg + 0.5));
   $PhaseDegStr = "Phase &nbsp; $PhaseDeg deg";

   $PhaseDeg4dText = sprintf("%3.4f", $PhaseDeg)." deg";


// Generate text description of lunar phase.
   $PhaseText = Lunar_Phase_to_Text ($PhaseDeg);

   $PhaseDegText = $PhaseDeg4dText;

// Patch to compute the relative lunar phase image size scale.
   $ImgWidthPx = sprintf("%1.2f", 382000/$DistKm)*134 . 'px';

// Generate HTML code to display scaled lunar image.
   $LunarPhaseIMG = "<img src=\"small_lunar_phases/phase_$PhaseXXX.png\" alt='' style='-o-transform:rotate($ParallAngRot);-ms-transform:rotate($ParallAngRot);transform:rotate($ParallAngRot);' width=\"$ImgWidthPx\">";


// Generate table(s) at 1-hour intervals from origin.
   $HourlyTableStr1 = Hourly_Lunar_Ephemeris_Table ($JDTTOrigin, $LETableStr);

   $HourlyTableStrX = Hourly_Lunar_Ephemeris_Table_X ($JDTTOrigin, $LETableStr, 2);


   $HourlyTableStr4 = Hourly_Lunar_Ephemeris_Table4 ($JDTTOrigin, $LETableStr, 2);
//   $HVTableStr4 = HRV_Table ($HourlyTableStr4);


   $HRVOVTableStr = Hourly_Lunar_Dist_RV_OV_Table ($HourlyTableStr4);

//   $HourlyDistTable = ''; // Hourly_Lunar_Dist_Diff_Table ($JDTTOrigin, $LETableStr);
//   $HourlyTableStr3 = Hourly_Lunar_Ephemeris_Table3 ($JDTTOrigin, $LETableStr, 2); // BUG ???


// Compute distance, vadial and orbital velocities for
// the given time.
   $RVOVStr = HMS_to_RV_OV ($hhmmss, $HRVOVTableStr);












   $ApogeePerigeeStr = Hourly_Apogee ($HourlyTableStrX);

// Compute all events for month for testing purposes

 if ($TimeMode == 'TT')
{
 $AllEventsTableStr = RTrim(Lunar_Phases_Apogees_Perigees ("$yyyy$mn", "+00:00", FALSE));
}
 if ($TimeMode == 'UT')
{
  $AllEventsTableStr = RTrim(Lunar_Phases_Apogees_Perigees ("$yyyy$mn", "+00:00", TRUE));
}
 if ($TimeMode == 'LT')
{
  $AllEventsTableStr = RTrim(Lunar_Phases_Apogees_Perigees ("$yyyy$mn", $TZhhmm, TRUE));
}

// REFORMAT AllEventsTableStr TO NEW REDUCED (TO NEAREST MINUTE) FORMAT.
// ROUND TO NEAREST MINUTE AND CUT OFF JDTT PART.
// OLD 2017 May 02 Tue 22:46:50 First Quarter Moon     Phase 90      2457876.61666968
// NEW 2017 May 02 Tue 22:47    First Quarter Moon     Phase 90

   $AllEventsTableStr = Reformat_Events_Table ($AllEventsTableStr);



// ------------------------------------------------------------
// COMPUTE SUN RISE/SET TIMES USING THE NATIVE PHP 5 FUNCTIONS.

//   $ZenithRise = 90.583333;
//   $ZenithSet  = 90.583333;

   $ZenithRise = 90 + 25/48; // orig = 90.583333
   $ZenithSet  = 90 + 20/31;

   $TZHrs  = HMS_to_Hours($TZhhmm);

// Patch for sunrise/sunset if TZ = 0 - part 1
   if ($TZHrs <> 0)
  {
   $w = new DateTime("$yyyy-$mn-$dn $hhmmss", new DateTimeZone($TZHrs));
   $sunrise = date_sunrise($w->getTimestamp(), SUNFUNCS_RET_STRING, $latitude, $longitude, $ZenithRise, $TZHrs);
   $sunset  = date_sunset($w->getTimestamp(),  SUNFUNCS_RET_STRING, $latitude, $longitude, $ZenithSet,  $TZHrs);
  }
   else
  {
   $w = new DateTime("$yyyy-$mn-$dn $hhmmss", new DateTimeZone('GMT'));
   $sunrise = date_sunrise($w->getTimestamp(), SUNFUNCS_RET_STRING, $latitude, $longitude, $ZenithRise, $TZHrs);
   $sunset  = date_sunset($w->getTimestamp(),  SUNFUNCS_RET_STRING, $latitude, $longitude, $ZenithSet,  $TZHrs);
  }

   /*
  {
   $sunrise = $sunset = '';
  }
*/




   $QPEventsString  = trim(Hourly_Q_Phase($HourlyTableStrX));
   $APEventsString  = Hourly_Apogee  ($HourlyTableStrX);
   $APEventsString .= Hourly_Perigee ($HourlyTableStrX);

   if (trim($QPEventsString) == '') {$QPEventsString = '';}
   if (trim($APEventsString) == '') {$APEventsString = '';}





// COMPUTE MOON RISE/SET TABLE FOR DATE

   $RSTableString = "";

   $AltTableTTStr     = Lunar_Altitude_Table ($JDNumTT, $LETableStr, $latitude, $longitude, $elevation);
   $RSTableTTStr      = Mark_Lunar_RS_Points ($AltTableTTStr);

   $RiseSetTableLTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, $TZhhmm, $TimeMode);
   $RiseSetTableUTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, '+00:00', 'UT');
   $RiseSetTableTTStr = Interp_Lunar_RS_Times_XX ($RSTableTTStr, '+00:00', 'TT');





// Filter Rise/Set table keeping only lines matching current date.
   $wArray = preg_split("[\n]", $RiseSetTableLTStr);
   $wCount = count($wArray);
   $w = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = $wArray[$i];
   if(intval(substr($CurrLine, 26,2)) == intval($dn)) {$w .= "$CurrLine\n";}
  }
   $RiseSetTableLTStr = RTrim($w);

// Filter Rise/Set table keeping only lines matching current date.
   $wArray = preg_split("[\n]", $RiseSetTableLTStr);
   $wCount = count($wArray);
   $w = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = $wArray[$i];
   if(intval(substr($CurrLine, 26,2)) == intval($dn)) {$w .= "$CurrLine\n";}
  }
   $RiseSetTableLTStr = RTrim($w);

// Tidy up Rise/Set text.
   $wArray = preg_split("[\n]", $RiseSetTableLTStr);
   $wCount = count($wArray);
   $w = '';
   for($i=0;   $i < $wCount;   $i++)
  {
   $CurrLine = RTrim($wArray[$i]);
   $A = substr($CurrLine, 17, 15);
   $B = substr($CurrLine, 33, 8);
   $B = Round_HMS($B, 'hm');
   $C = substr($CurrLine, -1);
   $C = "Moon" . (($C == 'R')? 'rise ':'set  ');
   $w .= "$C $A $B $TimeMode\n";
  }
   $RiseSetTableLTStr = RTrim($w);

   $uArray = preg_split("[\n]", $RiseSetTableLTStr);
   $uCount = count($uArray);
   $RSEventsStr  = '';
   for($i=0;   $i < $uCount;   $i++)
  {
   $uLine  = trim($uArray[$i]);
   $uEvent = trim(substr($uLine, 0,8));
   $uTime  = substr(substr($uLine, -8), 0,5);
   $RSEventsStr .= "$uTime       $uEvent\n";
  }
   $RSEventsStr = RTrim($RSEventsStr);

// Construct list of daily events, if any.
   $SunRiseSet = '';

// Patch for sunrise/sunset if TZ = 0 - part 2
   if ($sunrise <> '')
  {
   $SunRiseSet = "$sunrise       Sunrise\n$sunset       Sunset";
  }

   $DayEventsString = trim("$RSEventsStr\n$QPEventsString\n$APEventsString\n$SunRiseSet");

// INSERT CODE TO ATTACH VISUAL LUNAR PHASE TO MOONRISE/SET TEXT.
   $w = Lunar_Phase_to_Text($PhaseDeg);
   $DayEventsString = str_replace("Moonrise", "Moonrise - $w", $DayEventsString);

   if (StrPos($DayEventsString, "Moonrise") === FALSE)
  {
   $DayEventsString = str_replace("Moonset",  "Moonset - $w",  $DayEventsString);
  }

// IF MULTIPLE EVENTS, THEN SORT THEM ACCORDING
// TO TIME AND RECONSTRUCT DAY EVENTS LIST.
   $uArray = preg_split("[\n]", $DayEventsString);
   $uCount = count($uArray);
   sort($uArray);

   $uTableStr = '';
   for ($i=0;   $i < $uCount;   $i++)
  {
   $uLine = $uArray[$i];
   $uTableStr .= "$uLine\n";
  }
   $DayEventsString = trim($uTableStr);





// Construct date/time strings.
   $mNameStr = substr(MONTH_NAMES, 10*($m-1), 3);

// Format JD values and date strings.
   $JDLT = sprintf("%7.8f", $JDLT);
   $JDUT = sprintf("%7.8f", $JDUT);
   $JDTT = sprintf("%7.8f", $JDTT);

   $DateStrLT = JD_Z_to_DT ($JDLT, '+00:00', 2);
   $DateStrUT = JD_Z_to_DT ($JDUT, '+00:00', 2);
   $DateStrTT = JD_Z_to_DT ($JDTT, '+00:00', 2);

   $JDNumLT = floor($JDLT + 0.5);
   $JDNumUT = floor($JDUT + 0.5);
   $JDNumTT = floor($JDTT + 0.5);

   $UTMode = 'UT';
   $TTMode = 'TT';

   $SN = ($latitude  < 0)? 'deg S':'deg N';  if ($latitude  == 0){$SN = ' ';}
   $WE = ($longitude < 0)? 'deg W':'deg E';  if ($longitude == 0){$WE = ' ';}

   $LatDMS = Deg_to_DMS($latitude,  2, '+', 'd');
   $LonDMS = Deg_to_DMS($longitude, 2, '+', 'd');

// Format latitude and longitude values.
   $latitude  = sprintf("%+1.7f", $latitude);
   $longitude = sprintf("%+1.7f", $longitude);

   $LonHrs = $longitude/15;
   $LonHMS = Hours_to_HMS ($LonHrs, 2, '', 'h');
   $LonHrs = sprintf("%+1.7f", $LonHrs);

// Isolate date string without attached time string.
   $DateOnlyStrLT = substr($DateStrLT, 0,15);

   $RAHMS = Hours_to_HMS($RAHrs,   3,  '', 'h');
   $DeclDMS = Deg_to_DMS($DeclDeg, 2, '+', 'd');
   $DistMi = sprintf("%6.3f", $DistKm/1.609344);







// *******************************
// AUXILIARY INTERNAL COMPUTATIONS

// Compute Greenwich sidereal time (GSTTrue).
   $GSTDegMean = Mean_Sid_Time($JDTT);  // Compute Greenwich mean sidereal time
   $dRADeg     = dEquinox($JDTT); // Compute nutation
   $GSTDegTrue = $GSTDegMean + $dRADeg; // Apply nutation
   $GSTHrsMean = sprintf("%1.7f", $GSTDegMean / 15);
   $GSTHrsTrue = sprintf("%1.7f", $GSTDegTrue / 15);
   $GSTDegMean = sprintf("%1.7f", $GSTDegMean);
   $GSTDegTrue = sprintf("%1.7f", $GSTDegTrue);
   $GSTHMSMean = Hours_to_HMS($GSTHrsMean, 2, '', 'h');
   $GSTHMSTrue = Hours_to_HMS($GSTHrsTrue, 2, '', 'h');


// Compute true local sidereal time (LSTTrue).
   $LSTDegMean = Mean_Sid_Time($JDTT, $longitude);  // Compute local mean sidereal time
   $dRADeg     = dEquinox($JDTT); // Compute nutation
   $LSTDegTrue = $LSTDegMean + $dRADeg; // Apply nutation
   $LSTHrsMean = sprintf("%1.7f", $LSTDegMean / 15);
   $LSTHrsTrue = sprintf("%1.7f", $LSTDegTrue / 15);
   $LSTDegMean = sprintf("%1.7f", $LSTDegMean);
   $LSTDegTrue = sprintf("%1.7f", $LSTDegTrue);
   $LSTHMSMean = Hours_to_HMS($LSTHrsMean, 2, '', 'h');
   $LSTHMSTrue = Hours_to_HMS($LSTHrsTrue, 2, '', 'h');



// Compute elements required for topocentric computations.
   $ii = 6356755/6378140;
   $jj = $elevation/6378140;
   $kk = atan($ii*tand($latitude)); // Radians

   $RhoxSinPhiPrime = $ii*sin($kk) + $jj*sind($latitude);
   $RhoxCosPhiPrime = cos($kk) + $jj*cosd($latitude);

   $RhoxSinPhiPrime = sprintf('%+1.10f', $RhoxSinPhiPrime);
   $RhoxCosPhiPrime = sprintf('%+1.10f', $RhoxCosPhiPrime);



// Construct altitude and azimuth info string.
   $AltAzimStr = "$TopoAltDeg3&deg; $ABHorizon in $AzimDirSymbol";









/*
   Define HTML to display lunar statistics insert.
*/

$InsertMoonText =
"                    QUICK DE405 LUNAR EPHEMERIS STATISTICS

-------------------------
LOCAL DATE/TIME/MODE/ZONE

Date      =  $DateOnlyStrLT
Time      =  $hhmmss $TimeMode
Time Zone =  UT$TZhhmm
Delta T   = $dThhmmss

JD TT     =  $JDTT = Working Julian Date on TT scale

---------------------------
GPS COORDINATES OF LOCATION

Latitude  = $latitude $SN = $LatDMS
Longitude = $longitude $WE = $LonDMS = $LonHMS
Elevation = $elevation m

======================================================================
Phase Angle  = $PhaseDeg4dText
Visual Phase = $PhaseText

Horizon Altitude  = $TopoAltDeg3&deg; $ABHorizon in $AzimDirSymbol
Direction Azimuth = $TopoAzimDegN0&deg; = $AzimDirSymbol (N=0, E=90, S=180, W=270)

======================================================================
TIME  $TimeMode    LOCAL BASIC EVENTS SCHEDULE FOR DATE
--------    ------------------------------------
$DayEventsString

======================================================================
GEOCENTRIC LUNAR COORDINATES AND DISTANCE

Right Ascension =  $RAHMS =  $RAHrs h = $RADeg&deg;
Declination     = $DeclDMS  = $DeclDeg&deg;
Distance        =  $DistMi mi   =  $DistKm km

Angular Diameter    = $AngDiamStr = $AngDiamMM&#39; = $AngDiamSS&#34; = $AngDiamDeg&deg;
Eq. Horiz. Parallax = $HPDMS = $HPDeg&deg; = $HPMM&#39; = $HPSS&#34;
Local Hour Angle    = $LHAHMS = $LHAHrs h = $LHADeg&deg;

-----------------------------
TOPOCENTRIC LUNAR COORDINATES

Right Ascension  =  $TopoRAHMS =  $TopoRAHrs h = $TopoRADeg&deg;
Declination      = $TopoDeclDMS  = $TopoDeclDeg&deg;

$RVOVStr

$AllEventsTableStr
";

   $TextRowsA = 2 + substr_count($InsertMoonText, "\n");

   $InsertMoonText =
"<tr><td colspan='4' style='background-color:black; text-align:center;'>
<textarea ID='InsertMoonText' cols='81' rows='$TextRowsA' style='color:black; background-color:white;' readonly='readonly'>
$InsertMoonText
</textarea></td></tr>";





// COMPLETELY BLANK-OUT AP EVENTS STRING IF NO EVENTS.
   $APEventsStr = ($APEventsString <> '')? "TIME  $TimeMode    EVENT\n$APEventsString":'';

$APEventsStr =
"$APEventsStr
--------    -----------------------------------------------------------
";

// CONSTRUCT OUTPUT TEXT BLOCK.
   $OutputText =
"EXTENDED DE405 LUNAR EPHEMERIS STATISTICS AND HOURLY TABLES

Time Mode = $TimeMode
Time Zone = UT$TZhhmm

----------------------------------------------------------
CALENDAR DATE     TIME         JULIAN DATE     JD NUMBER
--------------- -----------  ----------------  -----------
$DateStrLT $TimeMode  $JDLT  $JDNumLT  $TimeMode
               $TZhhmm:00 TZ       $TZFracDay
$DateStrUT $UTMode  $JDUT  $JDNumUT  UT
               $dThhmmss dT       $dTFracDay
$DateStrTT $TTMode  $JDTT  $JDNumTT  TT

JD TT = $JDTT = Working Julian Date on TT scale

---------------------------
GPS COORDINATES OF LOCATION

Latitude  = $latitude $SN = $LatDMS
Longitude = $longitude $WE = $LonDMS = $LonHMS
Elevation = $elevation m

----------------------
APPARENT SIDEREAL TIME

Greenwich  $GSTHMSTrue = $GSTHrsTrue h = $GSTDegTrue deg
Local      $LSTHMSTrue = $LSTHrsTrue h = $LSTDegTrue deg

=================================================================
LUNAR POSITION, DISTANCE, PHASE AND LOCAL SUN/MOON RISE/SET TIMES

Date      =  $DateOnlyStrLT
Time      =  $hhmmss $TimeMode
Time Zone =  UT$TZhhmm
Delta T   = $dThhmmss
JD TT     =  $JDTT

GEOGRAPHIC LOCATION
Latitude  = $latitude $SN = $LatDMS
Longitude = $longitude $WE = $LonDMS = $LonHMS
Elevation = $elevation m

GEOCENTRIC LUNAR COORDINATES
Right Ascension =  $RAHMS =  $RAHrs h = $RADeg&deg;
Declination     = $DeclDMS  = $DeclDeg&deg;
Distance        =  $DistMi mi   =  $DistKm km

Phase Angle  = $PhaseDeg&deg;
Visual Phase = $PhaseText

Angular Diameter    = $AngDiamStr = $AngDiamMM&#39; = $AngDiamSS&#34; = $AngDiamDeg&deg;
Eq. Horiz. Parallax = $HPDMS = $HPDeg&deg; = $HPMM&#39; = $HPSS&#34;

$RVOVStr

TOPOCENTRIC LUNAR COORDINATES
Right Ascension  =  $TopoRAHMS =  $TopoRAHrs h = $TopoRADeg&deg;
Declination      = $TopoDeclDMS  = $TopoDeclDeg&deg;

Horizon Altitude = $AltAzimStr

Direction Azimuth = $TopoAzimDegN0&deg; = $AzimDirSymbol (N=0, E=90, S=180, W=270)

Local Hour Angle = $LHAHMS = $LHAHrs h = $LHADeg&deg;

Light Time = $LTSec sec = 1-way surface-to-surface
           = $LT2xSec sec = 2-way surface-to-surface

--------    ------------------------------------------------
TIME  $TimeMode    LOCAL EVENTS SCHEDULE FOR DATE

$DayEventsString

-----------------------------------------------------------------
HOURLY GEOCENTRIC LUNAR COORDINATES, DISTANCE AND PHASE EPHEMERIS

DATE =  $DateOnlyStrLT $TimeMode
TZ   =  UT$TZhhmm = Time Zone Offset From UT (Neg/Pos = West/East)
dT   = $dThhmmss = NASA Delta T estimate

Table Origin for 00:00:00 $TimeMode of date = $JDTTOrigin TT

$TimeMode
hh    RA Hrs     Decl Deg    Dist Km     Dist Mi    Phase Deg
--  ----------  ----------  ----------  ----------  ---------
$HourlyTableStrX

--------    -------------------------------------------------
TIME  $TimeMode    BASIC EVENTS SCHEDULE FOR DATE AND LOCATION

$DayEventsString

======================================================================
HOURLY GEOCENTRIC LUNAR DISTANCE, RADIAL VELOCITY AND ORBITAL VELOCITY
TABLE INCLUDING ANY APOGEE OR PERIGEE EVENT ON DATE.

DATE =  $DateOnlyStrLT
TIME =  UT$TZhhmm
dT   = $dThhmmss = NASA Delta T estimate

For Radial Velocity: Neg/Pos = Approaching/Receding

==  =======================  ===================    ====================
$TimeMode    GEOCENTRIC DISTANCE      RADIAL VELOCITY        ORBITAL VELOCITY
hh      km           mi        km/h       mi/h        km/h        mi/h
--  ----------   ----------  --------   --------    --------    --------
$HRVOVTableStr
$APEventsStr
";

}





// ------------------------------------------------------------
// DETERMINE HOW MANY ROWS TO ALLOCATE FOR MAIN OUTPUT TEXTAREA

   $TextRowsB = 2 + substr_count($OutputText, "\n");




print <<< _HTML

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

<html>
<head>

<meta http-equiv="content-type" content="text/html; charset=UTF-8">

<script type='text/javascript'>
function Get_TZ()
{
var d = new Date()
return d.getTimezoneOffset()/60;
}
</script>


<title>$_HTMLTitle_</title>

<style type='text/css'>
BODY  {color:silver; background-color:black;  font-family:Verdana; font-size:15px}
TABLE {font-size:12px; border: 1px solid black;}
TD    {color:black; background-color:white;  line-height:125%; font-size:11px; padding:6px; text-align:center;}
DIV   {color:black; background-color:white;  font-family:Verdana; text-align:justify; padding:4px; font-size:16px;}
PRE   {color:black; background-color:white;  font-family:monospace; font-size:14px; font-weight:bold; text-align:left; padding:4px;}

CODE  {font-family:monospace; font-size:15px; font-weight:normal;}

TEXTAREA {color:black; background-color:white; font-family:monospace; font-size:18px; font-weight:bold; box-shadow:4px 4px 3px #000000; border:1px solid black; border-radius:8px; padding:4px; white-space:pre;}

INPUT[type="text"]::-ms-clear {width:0; height:0;}
INPUT[type="text"] {background-color:white; box-shadow:2px 2px 3px #666666; font-family:monospace; font-size:16px; font-weight:bold; text-align:center; border:2px solid black;}
INPUT[type="text"]:focus {background-color:white; box-shadow:2px 2px 3px #666666; font-family:monospace; font-size:16px; border:2px solid red; text-align:center; font-weight:bold;}
INPUT[type="submit"]{font-weight:bold;}
INPUT[type="submit"]:hover {box-shadow:2px 2px 5px #222222; font-weight:bold;}
INPUT[type="checkbox"]:checked {box-shadow:2px 2px 3px #222222;}

HR {background-color:black; height:2px; border:0px;}

A:link,  A:visited {background-color:transparent; color:blue; font-family:Verdana; font-size:90%; font-weight:bold; text-decoration:none; line-height:150%; padding:2px;}
A:hover, A:active  {background-color:transparent; color:red;  font-family:Verdana; font-size:90%; font-weight:bold; text-decoration:none; line-height:150%; padding:2px;}

::selection      {background-color:yellow; color:black;}  /* Only affects IE9+ and Safari     */
::-moz-selection {background-color:yellow; color:black;}  /* Only affects FireFox and Mozilla */
</style>

<body>

<form name='form1' method='post' action="$RunThisProgram">
<table width='700' cellspacing='1'>

<tr><td colspan='4' style='color:white; background-color:#0000DD; font-weight:bold; border:2px solid white; border-radius:8px 8px 0px 0px;'>$_InterfaceTitle_</td></tr>

<tr><td><b>Calendar&nbsp;Date</b><br>Year&nbsp;/&nbsp;Month&nbsp;/&nbsp;Day</td><td><b>Time</b><br>hh:mm:ss</td><td><b>Date/Time&nbsp;Mode</b><br>LT | UT | TT</td><td><b>Local&nbsp;Time&nbsp;Zone&nbsp;Offset&nbsp;From&nbsp;UT</b><br>&plusmn;hh:mm (Neg/Pos &nbsp;=&nbsp; West/East)</td></tr>

<tr><td style='border-radius:0px 0px 0px 8px;'><input name='yyyy' type='text' size='5' maxlength='4' value="$yyyy" title='Year:&nbsp;1600&nbsp;to&nbsp;2200' style='border-radius:8px;'>&nbsp;<input name='mn' type='text' size='3' maxlength='2' value="$mn" title='Month number: 1 to 12' style='border-radius:8px;'>&nbsp;<input name='dn' type='text' size='3' maxlength='2' value="$dn" title='Day number: 1 to 31' style='border-radius:8px;'></td><td><input name='hhmmss' type='text' size='9' maxlength='8' value="$hhmmss" style='border-radius:8px;' title='00h to 24h'></td><td><input name='TimeMode' type='text' size='3' maxlength='2' value="$TimeMode" title=' LT | UT | TT ' style='border-radius:8px;'></td><td style='border-radius:0px 0px 8px 0px;'>Local&nbsp;Time&nbsp;(<b>LT</b>)&nbsp;=&nbsp;&nbsp;<b>UT</b><input name='TZhhmm' type='text' size='7' maxlength='6' value="$TZhhmm" style='text-align:left; border:none;border-style:solid; border-color:silver; border-width:1px 1px 1px 0px; box-shadow:none;' title='-12:00 to +12:00'></td></tr>

<tr><td colspan='4' style='color:white; background-color:black; font-size:12px;'>Default Location:&nbsp; <b>$STZStr</b> - Your Local Latitude, Lonitude and Time Zone May be Different</td></tr>

<tr><td colspan='3' style='color:white; background-color:#0000DD; font-weight:bold; border:2px solid white; border-radius:8px 0px 0px 0px;'>Geographic Location Used For Local Topocentric Computations</td><td rowspan='3' style='background-color:#000022; color:silver; border:1px solid #444444; border-radius:0px 8px 0px 0px;'>$LunarPhaseIMG<br><br><b>$PhaseDegText</b></td></tr>
<tr><td><b>Latitude</b><br>Deg.ddd&nbsp;or&nbsp;DMS&nbsp;(Neg=S)</td><td><b>Longitude</b><br>Deg.ddd&nbsp;or&nbsp;DMS&nbsp;(Neg=W)</td><td><b>Sea-Level&nbsp;Elevation</b><br>meters</td></tr>
<tr><td><input name='latitude' type='text' size='17' maxlength='16' value="$latitude" style='border-radius:8px;' title='-90 to +90 degrees'></td><td><input name='longitude' type='text' size='17' maxlength='16' value="$longitude" style='border-radius:8px;' title='0 to &plusmn;180 degrees'></td><td colspan='1'><input name='elevation' type='text' size='11' maxlength='10' value="$elevation" title='Sea-level Elevation in Meters ' style='border-radius:8px;'></td></tr>



<tr><td colspan='4' style='background-color:silver; border-radius:0px 0px 8px 8px;'><input name='ComputeButton' type='submit' value='Compute' OnClick="$StatusText1">&nbsp;&nbsp;&nbsp;<input name='InfoButton' type='submit' value='Info' title='View General Program Information'></td></tr>
$InsertMoonText
<tr><td colspan='4' style='background-color:black;'>
<textarea ID='OutputText' rows="$TextRowsB" cols='81' style="font-size:18px; color:$TX; background-color:$BG; border:1px solid white; border-radius:8px; padding:8px;" readonly='readonly'>
$OutputText
</textarea>
</td></tr>

<tr><td colspan='4' style='color:#666666; font-size:11px; border-radius:8px;'>$_HTMLTitle_ - $_ProgramVersion_ - A PHP Astronomy Program by Jay Tanner<br>
Revised: <code>$_RevisionDate_</code></td></tr>

</table>
</form>


</body>
</html>

_HTML;




// Compute hourly table for date starting at JDTT
// value corresponding to 00:00:00 LT of date.
//
// This table requires the LETable for the month
// from which it extracts the statistics for any
// given JDTT within the month-table range.
//
// Mode 1 = Basic 00 to 24-hour table.
// Mode 2 = Same table with 3 extra padding
// hours at each end for interpolations.

   function Hourly_Lunar_Ephemeris_Table_X ($JDTTOrigin, $LETableStr, $Mode=1)
{
   $k = 2*(((intval($Mode) < 2)? 1:2)-1);

   $HourlyTableStr = '';

   for($hour = -$k-1;   $hour <= 24+$k+2;   $hour++)
{
// Compute current JDTT for hh hours offset from TT origin.
   $JDhhTT = $JDTTOrigin + $hour/24;

// Compute lunar ephem stats for current JD offset from origin.
   $LunarStatsStr = Lunar_Ephem_Stats ($JDhhTT, $LETableStr);

// Fetch values of the returned statistics.
   list
  (
   $JDhhTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg
  ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for neatness.
   $JDhhTT   = sprintf("%7.8f",  $JDhhTT);
   $hh       = sprintf("%02d",   $hour);
   $RAHrs    = substr("          ".sprintf("%1.7f",  $RAHrs),   -10);
   $DeclDeg  = substr("          ".sprintf("%+1.6f", $DeclDeg), -10);
   $DistMi   = substr("          ".sprintf("%6.3f",  $DistKm/1.609344),  -10);
   $DistKm   = substr("          ".sprintf("%6.3f",  $DistKm),  -10);
   $PhaseDeg = substr("        ".sprintf("%1.4f",    $PhaseDeg), -8);

// Append current formatted ephemeris line to hourly table.
   $HourlyTableStr .= "$hh  $RAHrs  $DeclDeg  $DistKm  $DistMi  $PhaseDeg\n";
}

// Done
   return RTrim($HourlyTableStr);

   } // End of  Hourly_Lunar_Ephemeris_Table_X (...)















/*
 Compute hourly table for date starting at JDTT
 value corresponding to 00:00:00 LT of date.

 This table requires the LETable for the whole month
 from which it extracts the statistics for any given
 JDTT within the month-table range.

 Mode 1 = Basic 00 to 24-hour table.
 Mode 2 = Same table with extra padding
 hours at each end for interpolations.

 JDTTOrigin = JD TT corresponding to 00h LT of date
 LETableStr = Lunar ephemeris table for given month

*/


   function Hourly_Lunar_Ephemeris_Table4 ($JDTTOrigin, $LETableStr, $Mode=1)
{
   $k = 2*(((intval($Mode) < 2)? 1:2)-1);

   $HourlyTableStr = '';

   for($hour = -$k-0.5;   $hour <= 24+$k+2;   $hour+=0.5)
{
// Compute current JDTT for hh hours offset from TT origin.
   $JDhhTT = $JDTTOrigin + $hour/24;

// Compute lunar ephem stats for current JD offset from origin.
   $LunarStatsStr = Lunar_Ephem_Stats ($JDhhTT, $LETableStr);

// Fetch values of the returned statistics.
   list
  (
   $JDhhTT,
   $RAHrs,
   $DeclDeg,
   $DistKm,
   $PhaseDeg
  ) = preg_split("[ ]", strip_wspace($LunarStatsStr));

// Format computed statistics for tabulation.
   $JDhhTT   = sprintf("%7.8f",  $JDhhTT);
   $hh       = sprintf("%2.1f",   $hour); $hh = ($hh < 10 and $hh > -0.5)? "0$hh":$hh;
   $RAHrs    = substr("          ".sprintf("%2.7f",  $RAHrs),   -10);
   $DeclDeg  = substr("          ".sprintf("%+2.6f", $DeclDeg), -10);
   $DistMi   = substr("          ".sprintf("%6.3f",  $DistKm/1.609344), -10);
   $DistKm   = substr("          ".sprintf("%6.3f",  $DistKm),  -10);
   $PhaseDeg = substr("         ".sprintf("%3.5f",   $PhaseDeg), -9);

// Append current formatted ephemeris line to hourly table.
   $HourlyTableStr .= "$hh  $RAHrs  $DeclDeg  $DistKm  $DistMi $PhaseDeg\n";
}

// Done
   return RTrim($HourlyTableStr);

   } // End of  Hourly_Lunar_Ephemeris_Table4 (...)























// Compute hourly lunar distance, radial velocity and
// orbital velocity table.

   function Hourly_Lunar_Dist_RV_OV_Table ($HLETable4)
{
   $wArray = preg_split("[\n]", trim($HLETable4));
   $wCount = count($wArray);

// TABLE A:
   $TableAStr = '';
   for ($i=0;   $i < $wCount;   $i+=2)
  {
   $CurrLine = trim($wArray[$i]);
   $TableAStr .= "$CurrLine\n";
  }

// TABLE B:
   $TableBStr = '';
   for ($i=1;   $i < $wCount;   $i+=2)
  {
   $CurrLine = trim($wArray[$i]);
   $TableBStr .= "$CurrLine\n";
  }

// Make B Table array to match HRV table lines.
   $vArray = preg_split("[\n]", trim($TableBStr));
   $vCount = count($vArray);

   $uArray = preg_split("[\n]", trim($TableAStr));
   $uCount = count($uArray);

   $HRVTableStr = '';
   for ($i=1;   $i < $uCount;   $i++)
  {
   $CurrLine = trim($uArray[$i]);
    list
   (
    $Currhh,
    $CurrRAHrs,
    $CurrDeclDeg,
    $CurrDistKm,
    $CurrDistMi,
    $CurrPhaseDeg
   ) = preg_split("[ ]", strip_wspace($CurrLine));

   $PrevLine = trim($uArray[$i-1]);
    list
   (
    $Prevhh,
    $PrevRAHrs,
    $PrevDeclDeg,
    $PrevDistKm,
    $PrevDistMi,
    $PrevPhaseDeg
   ) = preg_split("[ ]", strip_wspace($PrevLine));





// Compute orbital velocity (OV)
   $OVkph = Dist_Point_A_to_Point_B ($CurrRAHrs,$CurrDeclDeg,$CurrDistKm, $PrevRAHrs,$PrevDeclDeg,$PrevDistKm);
   $OVmph = $OVkph / 1.609344;
   $OVkph = sprintf("%4.3f", $OVkph);
   $OVmph = sprintf("%4.3f", $OVmph);

   $OVStr = "$OVkph    $OVmph";

// Compute radial velocity (RV)
   $RVKm = sprintf("%+3.3f", $CurrDistKm - $PrevDistKm);
   $RVMi = sprintf("%+3.3f", $CurrDistMi - $PrevDistMi);
   $RVKm = substr("        $RVKm", -8);
   $RVMi = substr("        $RVMi", -8);
   $ii = sprintf("%02d", $i-3);

// Get distances substrings.
   $Dkm = substr($vArray[$i-1], 30,10);
   $Dmi = substr($vArray[$i-1], 42,10);
   $DistanceStr = "$Dkm   $Dmi";

/*
hh.h   RAHrs      DeclDeg     DistKm      DistMi       PhaseDeg
0,4    6,10       18,10       30,10       42,10        53,9
-2.5   2.9171564  +11.630142  359664.521  223485.172   7.77749
*/

   $HRVTableStr .= "$ii  $DistanceStr  $RVKm   $RVMi    $OVStr\n";
  } // End of for(...)

   return trim($HRVTableStr);
} // End of  Hourly_Lunar_Dist_RV_OV_Table(...)



/*
   Given the Hourly_Lunar_Dist_RV_OV_Table result, compute
   the specific (RV, OV) values for the given time.

   Known: Pre-computed D,RV,OV table
          HMS within table

   To Compute:
   D = Distance
   RV = Radial Velocity
   OV = Orbital velocity

*/

   function HMS_to_RV_OV ($HMSString, $RVOVTableString)
{

   $hours = HMS_to_Hours($HMSString);
   $hour  = floor($hours);

// Store D,RV,OV table in work array.
   $wArray = preg_split("[\n]", trim($RVOVTableString));
   $wCount = count($wArray);

// Set pointer to hour value within table.  This line will
// be the central line in the 5-point interpolation table
// used to compute the D, RV and OV values corresponding to
// the given time (hours).

   for($i=0;   $i < $wCount;   $i++)
  {
   if (intval(trim($wArray[$i])) == $hour) {break;}
  }




// Create 5 point D,RV,OV tables for interpolations.

   $DistKmTableStr = $RVkphTableStr = $OVkphTableStr = '';

   for($j = $i-2;   $j < $i+3;   $j++)
  {
   $CurrLine = trim($wArray[$j]);
    list
  (
   $hh,
   $DistKm,
   $DistMi,
   $RVkph,
   $RVmph,
   $OVkph,
   $OVmph
  ) = preg_split("[ ]", strip_wspace($CurrLine));

   $h = intval($hh);

// Construct D interpolation
   $DistKmTableStr .= "$h  $DistKm\n";
   $RVkphTableStr  .= "$h  $RVkph\n";
   $OVkphTableStr  .= "$h  $OVkph\n";

  } // End of for(...)

// return "$hour\n\n$DistKmTableStr\n\n$RVkphTableStr\n\n$OVkphTableStr";


// Perform Lagrange interpolations.
   $DistKm = Lagrange_Interp($DistKmTableStr, $hours);
   $RVkph  = Lagrange_Interp($RVkphTableStr,  $hours);
   $OVkph  = Lagrange_Interp($OVkphTableStr,  $hours);

// Compute equivalent DistMi/mph.
   $k = 1.609344;
   $DistMi = $DistKm / $k;
   $RVmph  = $RVkph  / $k;
   $OVmph  = $OVkph  / $k;

// Format computed values.
   $RVkph  = sprintf("%+4.2f", $RVkph);
   $RVmph  = sprintf("%+4.2f", $RVmph);
   $OVkph  = sprintf("%4.2f",  $OVkph);
   $OVmph  = sprintf("%4.2f",  $OVmph);

   $RVkph = substr("       $RVkph", -7);
   $RVmph = substr("       $RVmph", -7);
   $OVkph = substr("       $OVkph", -7);
   $OVmph = substr("       $OVmph", -7);

   $AppRecStr = ($RVkph < 0)? 'Approaching Earth':'Receding from Earth';
   if ($RVkph == 0) {$AppRecStr = 'Static';}

// Done.
   $w =
"Radial Velocity  = $RVkph km/h = $RVmph mi/h = $AppRecStr
Orbital Velocity = $OVkph km/h = $OVmph mi/h";

   $w = str_replace("-0.00", " 0.00", $w);
   $w = str_replace("+0.00", " 0.00", $w);

   return $w;
}






   function Reformat_Events_Table ($EventsTableStr)
{
   $wArray = preg_split("[\n]", $EventsTableStr);
   $wCount = count($wArray);

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

if (substr($CurrLine, 0,8) == 'Applied ')
{
$CurrLine = substr($CurrLine, 0,32);
}

       if (intval($CurrLine <> 0))
      {
       $Seg1 = substr($CurrLine,  0, 15); // Date
       $Seg2 = substr($CurrLine, 16,  8); // Time
       $Seg3 = substr($CurrLine, 25, 37); // Event

//     ROUND OFF TIME TO NEAREST MINUTE
       $Seg2 = Round_HMS ($Seg2, 'hm');

//     RECONSTRUCT ARRAY LINE IN NEW FORMAT.
       $CurrLine = "$Seg1 $Seg2  $Seg3";
      }
       $wArray[$i] = "$CurrLine\n";
  }


// ============================================
// RECONSTRUCT AllEventsTableStr in new format.

   $wTable = '';

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

   $AllEventsTableStr = str_replace('Date and Time LT                 Lunar Event               Julian Date TT',   '   Date and Time LT               Lunar Event',  $AllEventsTableStr);

   $AllEventsTableStr = str_replace('Date and Time UT                 Lunar Event               Julian Date TT', '   Date and Time UT               Lunar Event',  $AllEventsTableStr);

   $AllEventsTableStr = str_replace('Date and Time TT                 Lunar Event               Julian Date TT', '   Date and Time TT               Lunar Event',  $AllEventsTableStr);

   $AllEventsTableStr = str_replace('------------------------ -----------------------------------  ----------------','---------------------  -----------------------------------',  $AllEventsTableStr);

   $AllEventsTableStr = str_replace('  Phase ', '= Phase ',  $AllEventsTableStr);

   $AllEventsTableStr = str_replace('Julian Date TT', '',  $AllEventsTableStr);

   return $AllEventsTableStr;
}








// ****************************************************************
// ****************************************************************
// ****************************************************************
// ****************************************************************
// ****************************************************************


   FUNCTION PHPSCILABS_Info()
{
   $cYear = gmdate("Y");

/*
  USE GLOB TO CONSTRUCT LIST OF AVAILABLE MODULES

  Define a list of module names and then
  store them in a work array. Modules may
  be added or removed from this list, but
  note that in certain cases, some modules
  may require functions in other modules.

*/

   $modules = "PHPSCILABS.php\n";

   ForEach (glob("*_Module.php") as $FileName)
  {
   $modules .= "$FileName\n";
  }



// Convert module names listing into an indexed work array
// with the same name.
   $modules = preg_split("[ ]", strip_wspace($modules));

// =========================================================================

// Get modules count.
   $ModulesCount = count($modules);

   $out =
"
<!doctype html public '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>

<html>

<head>

<title>PHP Science Labs Module Contents</title>

<style type='text/css'>
BODY {background-color:white; color:black; font-family:Verdana; font-size:100%;}

TEXTAREA{font-family:monospace; font-size:125%; font-weight:bold; background-color:LightYellow; color:black; border:1px solid black; border-radius:8px; box-shadow:2px 3px 4px #666666;}
</style>
</head>

<b><span style='font-family:Verdana; font-size:90%;'>
A quick listing of the current contents of the PHPSCILABS function module(s).</span></b>
<br><br>
";

   for ($i=0;   $i < $ModulesCount;   $i++)
       {
        $out .= Module_Info($modules[$i]);
       }
   return $out . "<br><br>\n</body>\n</html>\n";
   }

/*
   Scans and returns a listing of all custom definitions
   and functions found within the given module.

*/

   FUNCTION Module_Info ($ModuleFile='PHPSCILABS.php')
{
// Define path to PHP modules folder.
//   $ModuleFilePath = "modules/$ModuleFile";
   $ModuleFilePath = trim($ModuleFile);

// Load module file into work array and count
// the total number of text lines in the file.
   $ModuleLines = file($ModuleFilePath);
   $RevisedStr  = date("Y F d  h:i:s A  l", FileMTime($ModuleFilePath));
   $ModuleLinesCount = count($ModuleLines);

// This shouldn't be here, if it is, so it won't be.
   $ModuleFileName = str_replace("../",   "", $ModuleFilePath);


// ----------------------------------------------------
// Generate list of all custom constants within module.

   $ConstantsList  = '';
   $ConstantsCount = 0;

   for ($i=0;   $i < $ModuleLinesCount;   $i++)
       {
        $ModuleLine = trim($ModuleLines[$i]);
        $ModuleLine = str_replace(' ', '', $ModuleLine);

        if (StrToLower(substr($ModuleLine, 0, 8)) == 'define("')
           {
            $w = trim(substr($ModuleLine, 8, strlen($ModuleLine)));
            $w = str_replace('",', " = ", $w);

            $ParenthesisPointerR = strpos($w, ")");

            $w = substr($w, 0, $ParenthesisPointerR);

            $ConstantsList .= " $w\n\n";

            $ConstantsCount++;
           }
       }


// ----------------------------------------------------
// Generate list of all custom functions within module.
// To prevent a function from being listed, change the
// function prefix to all upper case, like FUNCTION().

   $FunctionsList  = '';
   $FunctionsCount = 0;

   for ($i=0;   $i < $ModuleLinesCount;   $i++)
       {
        $ModuleLine = trim($ModuleLines[$i]);

        if (substr($ModuleLine, 0, 8) == 'function')
           {
            $ModuleLine = trim(substr($ModuleLine, 8, strlen($ModuleLine)));

            $FunctionsList .= " $ModuleLine\n\n";

            $FunctionsCount++;
           }
       }

   $FunctionsList = trim($FunctionsList);

   $s1 = $s2 = '';

   $IsAre1 = $IsAre2 = 'is';

   if ($ConstantsCount <> 1)
  {
   $s1 = 's';   $IsAre1 = 'are';
   if ($ConstantsCount == 0) {$ConstantsCount = 'No';}
  }

   if ($FunctionsCount <> 1)
  {
   $s2 = 's';   $IsAre2 = 'are';
   if ($FunctionsCount == 0) {$FunctionsCount = 'No';}
  }

   $ConstantsList = trim($ConstantsList);

// -------------------------------------------------
// Construct return final html TEXTAREA code string.


   $ModuleFileName = str_replace('modules/', '', $ModuleFileName);

   $_BAR_ = str_repeat("&#151;", 76);

   $w =
"
 $ModuleFileName

 Revised:
 $RevisedStr


 MODULE CONTENTS LISTING:
 $_BAR_
 $ConstantsCount constant$s1 $IsAre1 defined in this module.

 $ConstantsList


 $_BAR_
 $FunctionsCount function$s2 $IsAre2 defined in this module.

 $FunctionsList
 ";

   $NumRows = 2 + substr_count($w, "\n");

 $w =
"
<textarea cols='75' rows=\"$NumRows\" readonly='readonly' style='font-size:14px; white-space:pre;'>
$w
</textarea>
<br><br>
";

   return $w;

} // End of  Module_Info (...)







?>


