<?php
/*
This program simply computes the basic ephemeris statistics and
simple phase of the moon for any date, time and location over a
span of several thousands of years.
The computations are based on the NASA/JPL Horizons API.
Range:
BC 9999-MAR-21 00:00:00.0006 TT to AD 9999-DEC-30 12:00:00.0011 TT
BC 9999-MAR-15 21:49:54.7144 UT to AD 9999-DEC-30 11:58:50.8171 UT
NOT ALL ARGUMENT ERRORS ARE CAUGHT BY THIS INTERFACE. SOME ARE RUN
TIME ERRORS ONLY DETECTED WHEN PASSED TO THE NASA/JPL HORIZONS API
WHICH SHOULD CATCH AND REPORT THEM.
Author : Jay Tanner - 2024
Language : PHP v7.4.9
License : Public Domain
*/
// --------------------------------------------------------
// Initialize output buffer.
ob_start();
// --------------------------------------
// Current year (from system clock time).
$cYear = date('Y');
// ----------------------------------------------------------------
// Define the program cookie name and set it to expire in 30 days.
$CookieName = '20000-Year-Moon-Phase-Calculator';
$ExpiresIn30Days = time() + 30*86400;
// ------------------------------------------------
// Define author, working script path and filename.
$_AUTHOR_ = "Jay Tanner";
$_PROGRAM_VERSION_ = "v1.0 - ";
$at = "at Local Time ";
$LTC = "UTC";
$_SCRIPT_PATH_ = Filter_Input(INPUT_SERVER, 'SCRIPT_FILENAME');
// -----------------------------------------
// Define internal document page title, HTML
// page heading text and revision date
$_BROWSER_TAB_TEXT_ = "20,000-Year Moon Phase Calculator";
$_INTERFACE_TITLE_ = "\n<span style='font-size:15pt;'>20,000-Year Moon Phase Calculator for Any Date, Time and Location</span><br>\n<span style='font-size:11pt; line-height:175%;'>BC 9999-Mar-22-Fri to AD 9999-Dec-29-Wed<br></span><br>\n<span style='font-size:10pt;'>PHP Program by $_AUTHOR_ - $cYear - Built Around The NASA/JPL Horizons API</span>\n";
$_REVISION_DATE_ = $_PROGRAM_VERSION_ .'Revised: '. date("Y-F-d-l $at h:i:s A ($LTC", FileMTime($_SCRIPT_PATH_))."−05:00)";
// Define JavaScript message to display while working.
// This is attached to the [COMPUTE] button.
$_COMPUTING_ = "TextArea1.innerHTML=' W.O.R.K.I.N.G --- This may take several seconds.';";
// -------------------------------------
// Define main TextArea text, background
// colors and default HTML column span.
$TxColor = 'black';
$BgColor = 'white';
$ColSpan = 99;
$PhaseAngImg = 'error.png';
// -------------------------------------------
// Define default home location coordinates
// in degrees and elevation in kilometers.
// If you have a talking homing pigeon, ask
// it for your home coordinates or ask Google.
// -------------------------------------------
$HomeLoc = 'Central New York State, USA';
$HomeTZ = '-05:00'; // Negative = West
$HomeLon = '-76.862737'; // Negative = West
$LonDeg = $HomeLon;
$HomeLat = '+42.904788'; // Negative = South
$LatDeg = $HomeLat;
$HomeAlt = '+150'; // Meters - Relative to Sea-Level
/*
$HomeLoc = 'New York City, USA';
$HomeTZ = '-05:00'; // Negative = West
$HomeLon = $LatDeg = '-73.935242'; // Negative = West
$LonDeg = $HomeLon;
$HomeLat = $LonDeg = '+40.730610'; // Negative = South
$LatDeg = $HomeLat;
$HomeAlt = '+53'; // Meters - Relative to Sea-Level
*/
// -------------------------------------------------
// Do this only if the [COMPUTE] button was clicked.
$w = Filter_Input(INPUT_POST, 'ComputeButton');
if (!IsSet($w))
{
// -----------------------------------------------------
// If [COMPUTE] button was clicked AND an active cookie
// exists, then restore the previous interface settings
// from it. Otherwise, set the default startup values.
$w = Filter_Input(INPUT_COOKIE, $CookieName);
if (IsSet($w))
{
$CookieDataString = Filter_Input(INPUT_COOKIE, $CookieName);
list
(
$LocName,
$TimeZone,
$DSSTYN,
$StartBCAD,
$StartYear,
$StartMonth,
$StartDay,
$StartTime,
$TimeScale,
$Longitude,
$Latitude,
$AltM
) = Preg_Split("[\|]", $CookieDataString);
$BodyID = '301';
$LonDeg = trim($Longitude);
$LatDeg = trim($Latitude);
}
else
// -------------------------------------------------
// SET THE INITIAL DEFAULT INTERFACE STARTUP VALUES.
{
$BodyID = '301'; // Force to Moon only
$LocName = $HomeLoc;
$TimeZone = $HomeTZ;
$DSSTYN = 'No';
$StartBCAD = 'AD';
$StartYear = date('Y');
$StartMonth = date('M');
$StartDay = date('d');
$StartTime = date('H:i:s');
$TimeScale = 'UT';
$Longitude = $HomeLon;
$LonDeg = $HomeLon;
$Latitude = $HomeLat;
$LatDeg = $HomeLat;
$AltM = $HomeAlt;
// -----------------------------------
// Store interface settings in cookie.
$CookieDataString = "$LocName|$TimeZone|$DSSTYN|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$TimeScale|$Longitude|$Latitude|$AltM";
SetCookie ($CookieName, $CookieDataString, $ExpiresIn30Days);
}
} // End of if (!IsSet(...))
// ---------------------------------------------
// READ VALUES OF ALL INTERFACE ARGUMENTS. EMPTY
// VALUES ARE SET TO THE DEFINED DEFAULT VALUES.
//
// NOTE:
// The @ symbols suppress the warning messages,
// which are actually unnecessary in this case.
$w = Filter_Input(INPUT_POST, 'ComputeButton');
if (isset($w))
{
$BodyID = '301'; // Forced to moon (Luna) only.
$LocName = trim(Filter_Input(INPUT_POST, 'LocName'));
if ($LocName == '') {$LocName = '-';}
$TimeZone = trim(Filter_Input(INPUT_POST, 'TimeZone'));
if ($TimeZone == '') {$TimeZone = $HomeTZ;}
@ $TimeZone = $TZHours = HMS_to_Hours($TimeZone);
@ $TimeZone = substr(Hours_to_HMS($TimeZone, 0, '+', ':'),0,6);
$DSSTYN = Substr(StrToUpper(trim(Filter_Input(INPUT_POST, 'DSTYN'))),0,1);
if ($DSSTYN == '') {$DSSTYN = 'N';}
$DSSTYN = ($DSSTYN == 'N')? 'No':'Yes';
$StartBCAD = Substr(StrToUpper(trim(Filter_Input(INPUT_POST, 'StartBCAD'))),0,1);
if ($StartBCAD == '') {$StartBCAD = 'AD';}
$StartBCAD = ($StartBCAD == 'B')? 'BC':'AD';
$StartYear = trim(Filter_Input(INPUT_POST, 'StartYear'));
if ($StartYear == '') {$StartYear = date('Y');}
if (Is_Numeric($StartYear)) {SPrintF("%04d", $StartYear);}
$StartMonth = trim(Filter_Input(INPUT_POST, 'StartMonth'));
if ($StartMonth == '') {$StartMonth = date('M');}
$StartMonth = Month_To_Str($StartMonth);
$StartDay = trim(Filter_Input(INPUT_POST, 'StartDay'));
if ($StartDay == '') {$StartDay = date('d');}
if (Is_Numeric($StartDay)) {$StartDay = SPrintF("%02d", $StartDay);}
$StartTime = trim(Filter_Input(INPUT_POST, 'StartTime'));
if ($StartTime == '') {$StartTime = date('H:i:s');}
@ $StartTime = HMS_to_Hours($StartTime);
@ $StartTime = Hours_to_HMS($StartTime, 0, '', ':');
$TimeScale = StrToUpper(substr(trim(Filter_Input(INPUT_POST, 'TimeScale')),0,1));
if ($TimeScale == '') {$TimeScale = 'UT';}
$TimeScale = ($TimeScale == 'T')? 'TT':'UT';
$Longitude = trim(Filter_Input(INPUT_POST, 'Longitude'));
if ($Longitude == '') {$Longitude = $HomeLon;}
@ $Longitude = DMS_to_Deg($Longitude);
$LonDeg = $Longitude;
$Longitude = SPrintF("%+1.9f", $Longitude);
$Latitude = trim(Filter_Input(INPUT_POST, 'Latitude'));
if ($Latitude == '') {$Latitude = $HomeLat;}
@ $Latitude = DMS_to_Deg($Latitude);
$LatDeg = $Latitude;
$Latitude = SPrintF("%+1.9f", $Latitude);
$AltM = trim(Filter_Input(INPUT_POST, 'AltM'));
if ($AltM == '') {$AltM = 0.0;}
$AltM = FloatVal($AltM);
$AltM = (($AltM < 0)? '-':'+').abs($AltM);
// ------------------------------------
// Store interface arguments in cookie.
$CookieDataString = "$LocName|$TimeZone|$DSSTYN|$StartBCAD|$StartYear|$StartMonth|$StartDay|$StartTime|$TimeScale|$Longitude|$Latitude|$AltM";
SetCookie ($CookieName, $CookieDataString, $ExpiresIn30Days);
}
// -----------------------------------------------------------------------------
// Construct proper date and time string to be used with NASA/JPL Horizons API.
// After this point, errors should be caught and reported by the Horizons API.
$StartDate = "$StartBCAD $StartYear $StartMonth $StartDay";
// -------------------
// Set for UTC or UT1.
$xTimeScale = '';
if ($TimeScale == 'UT')
{
$xTimeScale .= ($StartBCAD == 'AD' and $StartYear > 1961)? '1':'C';
}
// ###########################################################################
// BEGIN MAIN COMPUTATIONS CODE STARTING HERE AND PUT RESULTS INTO TEXT AREAS.
// THE @ SYMBOLS SUPPRESS THE WARNING MESSAGES CAUSED BY SOME INTERNAL ERRORS.
// --------------------------------------------
// Generate the raw topocentric ephemeris data.
// This raw data is not displayed but can be if
// you wish to see its content. The same con-
// tent is reformatted for easier printing.
$RawEphemerisData = Moon_Phase($StartDate,$StartTime,$TimeScale,$TimeZone,
$DSSTYN,$LonDeg,$LatDeg,$AltM);
// ------------------------------------------
// Compute moon rise/transit/set times table.
$RTSTimesTable = RTS_Times_Table ('301',$StartDate,$TimeScale,$TimeZone,
$DSSTYN,$LonDeg,$LatDeg,$AltM);
// ------------------------------------------------------------
// Return empty string if no ephemeris statistics are returned.
if ($RawEphemerisData == $RTSTimesTable) {$RTSTimesTable = '';}
// --------------------------------------------------------
// Extract the ephemeris work table for subsequent parsing.
// It is possible that an error or other message may be re-
// turned instead.
$TopoEphemWorkData = Extract_Ephem_Data($RawEphemerisData);
$EphemStatsData = Ephem_Stats_Table($RawEphemerisData);
// ---------------------------
// Patch to catch date errors.
$EphemStatsData = Str_Replace('Cannot interpret date. Type "?!" or try YYYY-MMM-DD {HH:MN} format.','ERROR: Invalid calendar date.', $EphemStatsData);
// --------------------------------------
// Construct output TextArea1 text block.
$TextArea1Text =
"======================================================
TOPOCENTRIC MOON PHASE CALCULATOR FOR
ANY GIVEN DATE, TIME AND LOCATION
Calendar Date/Time
$StartBCAD $StartYear-$StartMonth-$StartDay $StartTime
Time Scale: $xTimeScale
Time Zone UT$TimeZone (Ignored if TT Scale)
$DSSTMssg
Location Name
$LocName
Longitude = $Longitude°
Latitude = $Latitude°
Altitude = $AltM m
Reference Frame ICRF
International Celestial Reference Frame
Mean refraction is applied to rise/set computations.
$RTSTimesTable
$EphemStatsData";
// --------------------------------------
// Construct output TextArea2 text block.
$TextArea2Text = '';
// ---------------------------------------------------------------------------
// Determine number of text columns and rows to use in the output text areas.
// These valuse will vary randomly according to the text block width and length.
// This way you do not have to keep rewriting code to update the changing sizes
// of the text areas manually.
$Text1Cols = 1 + Max(Array_Map('StrLen', PReg_Split("[\n]", trim($TextArea1Text))));
$Text1Rows = 2 + Substr_Count($TextArea1Text, "\n");
if ($Text1Cols < 81) {$Text1Cols = 55;}
$Text2Cols = 1 + Max(Array_Map('StrLen', PReg_Split("[\n]", trim($TextArea2Text))));
$Text2Rows = 2 + Substr_Count($TextArea2Text, "\n");
if ($Text2Cols < 91) {$Text2Cols = 80;}
// --------------------------------------
// Round radial velocities to 3 decimals.
$RadVkph = SPrintF("%+1.3f", $RadVkph);
$RadVmiph = SPrintF("%+1.3f", $RadVmiph);
// --------------------------------------------------
// Generate client web page to display the ephemeris.
print <<< _HTML
<!DOCTYPE HTML>
<HTML lang='en-us'>
<head>
<title>$_BROWSER_TAB_TEXT_</title>
<meta name='viewport' content='width=device-width, initial-scale=0.8'>
<meta http-equiv='content-type' content='text/html; charset=UTF-8'>
<meta http-equiv='pragma' content='no-cache'>
<meta http-equiv='expires' content='-1'>
<meta name='description' content='JPL, NASA,Horizons,current moon phase, current lunar phase'>
<meta name='keywords' content='PHPScienceLabs.com'>
<meta name='author' content='Jay Tanner - https://www.PHPScienceLabs.com'>
<meta name='robots' content='noindex,nofollow'>
<meta name='googlebot' content='noindex,nofollow'>
<style type='text/css'>
BODY {color:white; background:#000020; font-family:Verdana; font-size:12pt; line-height:125%;}
TABLE
{font-size:13pt; background:#000020; border: 1px solid black;}
TD
{
color:black; background:white; line-height:150%; font-size:10pt;
padding:6px; text-align:center;
}
UL
{font-family:Verdana; font-size:12pt; line-height:150%; text-align:justify;}
PRE
{
background:white; color:black; font-family:monospace; font-size:12.5pt;
font-weight:bold; text-align:left; line-height:125%; padding:6px;
border:2px solid black; border-radius:8px;
page-break-before:page;
}
DIV
{
font-family:Verdana; font-size:11pt; font-weight:normal;
line-height:125%; padding:6px;
}
TEXTAREA
{
background:black; color:silver; font-family:monospace; font-size:11pt;
font-weight:bold; padding:4pt; white-space:pre; border-radius:8px;
line-height:125%;
}
INPUT[type='text']::-ms-clear {width:0; height:0;}
INPUT[type='text']
{
font-family:monospace; color:black; background:white; font-size:12pt;
font-weight:bold; text-align:center; box-shadow:2px 2px 3px #666666;
border:2px solid black; border-radius:4px;
}
INPUT[type='text']:focus
{
font-family:monospace; background:white; box-shadow:2px 2px 3px #666666;
font-size:12pt; border:2px solid blue; text-align:center; font-weight:bold;
border-radius:4px;
}
INPUT[type='submit']
{
background:black; color:#777777; font-family:Verdana; font-size:12pt;
font-weight:bold; border-radius:4px; border:4px solid #777777;
padding:3pt;
}
INPUT[type='submit']:hover
{
background:black; color:white; font-family:Verdana; font-size:12pt;
font-weight:bold; border-radius:4px; border:4px solid GreenYellow;
padding:3pt;
}
// ----------------------------------------
// Link states MUST be set in the following
// sequential order to work properly:
// :link, :visited, :hover, :active
A:link
{
font-size:10pt; background:transparent; color:#8080FF; border-radius:4px;
font-family:Verdana; font-weight:bold; text-decoration:none;
line-height:175%; padding:3px; border:1px solid transparent;
}
A:visited
{
font-size:10pt; background:transparent; color:DodgerBlue; border-radius:4px;
}
A:hover
{
font-size:10pt; background:yellow; color:black; border:1px solid black;
box-shadow:1px 1px 3px #222222; border-radius:4px;
}
A:active
{
font-size:10pt; background:yellow; color:black; border-radius:4px;
}
HR {background:red; height:4px; border:0px;}
[title-text]:hover:after
{
opacity:1.0;
transition:all 1.0s ease 1.0s;
text-align:left;
visibility:visible;
}
[title-text]:after
{
opacity:1.0;
content:attr(title-text);
text-align:left;
left:50%;
background-color:yellow;
color:black;
font-size:10pt;
position:absolute;
padding:1px 5px 2px 5px;
white-space:pre;
border:1px solid red;
z-index:1;
visibility:hidden;
}
[title-text] {position: relative;}
::selection{background-color:yellow !important; color:black !important;}
::-moz-selection{background-color:yellow !important; color:black !important;}
</style>
</head>
<body>
<form name='form1' method='post' action="">
<table width='790' align='top' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="99" style='color:white; background-color:#000066; border:2px solid white; border-radius:8px 8px 0px 0px;'>$_INTERFACE_TITLE_</td>
</tr>
</table>
<table width='790' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr title-text=' This can be any optional location name or lable text. '>
<td style='text-align:center; background:LightCyan; line-height:175%;'><b>Optional Location Name</b><br><input name='LocName' type='text' value="$LocName" size='56' maxlength='55'></td>
</tr>
</table>
<!-- Geographic (GPS) Coordinates --->
<table width='790' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td style='text-align:center; background:#CFFFCF; line-height:300%;'><div title-text=' Coordinates can be entered as decimal degrees or \n as Deg Min Sec separated by spaces. \n\n Make sure Geographic Coordinates are within the \n given Time Zone or the Rise/Set times may not be \n accurate. \n\n For time zones and longitudes, +Positive = East'><b>Geographic GPS Coordinates</b></div>
Longitude <input name='Longitude' type='text' value="$Longitude" size='15' maxlength='14' title=' Decimal Degrees or Deg Min Sec Separated by Spaces\n +Positive = East '>
Latitude <input name='Latitude' type='text' value="$Latitude" size='15' maxlength='14' title=' Decimal Degrees or Deg Min Sec Separated by Spaces\n Negative = South '>
Altitude <input name='AltM' type='text' value="$AltM" size='8' maxlength='9' title=' Elevation in Meters Relative to Sea Level '> m
</td>
</tr>
</table>
<!-- Time Zone Offset From UT --->
<table width='790' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan='3' style='text-align:center; background:LightYellow;'>Time Zone ?
<input name='TimeZone' type='text' value="$TimeZone" size='7' maxlength='6' title=' Local time zone offset from UT 00:00 = UT \n +Positive = East \n\n NOTE: Time Zone offset is ignored if using TT scale. '> Time Scale ?
<input name='TimeScale' type='text' value="$TimeScale" size='3' maxlength='2' title=' UT = Universal Time Scale (Default) \n TT = Terrestrial (Barycentric Dynamical) Time Scale '>
Daylight / Summer Time ? <input name='DSTYN' type='text' value="$DSSTYN" size='4' maxlength='3' title-text=' Daylight Saving / Summer Time ? \n\n No = Use Standard Time (Default) \n This Daylight/Summer Time setting applies ONLY to the UT time scale. \n This Daylight/Summer Time setting is IGNORED if using the TT time scale. '>
</td>
</tr>
</table>
<!-- Date and Time --->
<table width='790' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr title-text=' ALL BLANK = Current Date/Time '>
<td colspan='3' style='text-align:center; background:white; line-height:175%;'><b> Date and Time</b><br>
<input name='StartBCAD' type='text' value="$StartBCAD" size='3' maxlength='2'><input name='StartYear' type='text' value="$StartYear" size='5' maxlength='4'><input name='StartMonth' type='text' value="$StartMonth" size='4' maxlength='3'><input name='StartDay' type='text' value="$StartDay" size='3' maxlength='2'>
<input name='StartTime' type='text' value="$StartTime" size='13' maxlength='12'>
</td>
</tr>
</table>
<!-- Compute Button --->
<table width='790' align='bottom' border='0' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="$ColSpan" style='background:#000020;'>
<input name='ComputeButton' type='submit' value=' C O M P U T E ' OnClick="$_COMPUTING_">
</td>
</tr>
</table>
<!-- Moon phase image to be displayed. --->
<table width='790'>
<tr>
<td colspan="99" style='color:white; background:black; text-align:center; border:1px solid blue;'>
<span style='color:gray; font-size:8pt;'>N</span><br>
<img src='lunar-phases/$PhaseAngImg'><br>
<span style='color:silver; font-size:9pt;'>Phase Angle: $PhaseAngDeg°<br>$PhaseText<br>Distance: $DistKm km = $DistMi mi<br>
</span>
</td>
</tr>
</table>
<!-- Source Code View and/or Download Link. --->
<table width="790">
<tr>
<td colspan="1" style="font-size:10pt; color:GreenYellow; background:#000022; text-align:center;">
<b><a href='View-Source-Code.php' target='_blank' style='text-decoration:none; padding:4px;'>View Source Code</a></b>
<br>
<div style='text-align:center;'>
<b><a href='../20000-Year-Moon-Phase-Calculator-v1.7z' style='font-size:10pt; text-decoration:none; padding:4px;' target='_blank'>Download This Program + Phase Images (Approx. 15 MB)</a></b>
</div>
</td>
</tr>
</table>
<table width='790' border='0' align='bottom' cellspacing='1' cellpadding='3'>
<tr>
<td colspan="$ColSpan" style='background:#000020; text-align:center;'>
<div style='color:lime; font-size:10pt;'>Double-Click Within Text Area to Select ALL Text</div>
<textarea name='TextArea1' style='color:silver; background:#001800; padding:8px; border:2px solid green;' cols="$Text1Cols" rows="$Text1Rows" ReadOnly OnDblClick='this.select();' OnMouseUp='return true;'>
$TextArea1Text
</textarea></td>
</tr>
<!--
<tr>
<td colspan="$ColSpan" style="background:black; text-align:center;">
<div style='color:lime; font-size:10pt; text-align:left;'>Double-Click Within Text Area to Select ALL Text</div>
<textarea name='TextArea2' style='color:black; background:white; padding:6px;' cols="$Text2Cols" rows="$Text2Rows" ReadOnly OnDblClick='this.select();' OnMouseUp='return true;'>
$TextArea2Text
</textarea><br>
<span style='color:silver; background:black;'>PHP Program by $_AUTHOR_ $_REVISION_DATE_</span>
</td>
</tr>
--->
</table>
</form>
<br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br>
</body>
_HTML;
// ####################################################################
// CUSTOM UTILITY FUNCTIONS CODE CAN START HERE, BEYOND THE HTML ABOVE.
/*
============================================================================
This function converts a time elements (HMS) string to decimal hours.
The time argument elements can be separated by colons or spaces.
Examples: 'hh:mm:ss' or 'hh mm ss'
Time strings can be entered in several formats.
Where:
1 element = 'Hours'
2 elements = 'Hours Minutes'
3 elements = 'Hour Minutes Seconds'
NO DEPENDENCIES
###########################################################################
*/
function HMS_to_Hours ($hhmmss)
{
// ----------------------------------------
// Set internal working decimals precision.
$Q = 32;
// --------------------------
// Read time string argument.
$hms = trim($hhmmss);
// -------------------------------
// Replace any colons with spaces.
$hms = Str_Replace(':', ' ', $hms);
// ------------------------------------------------
// Normalize the spacing and then split and extract
// the individual time string elements (hh, mm,ss).
// Any arguments beyond the third will be ignored.
$hms = PReg_Replace("/\s+/", " ", trim($hms));
$wHMS = PReg_Split("[ ]", $hms);
$wHMScount = count($wHMS);
$hh = ($wHMScount >= 1)? bcAdd($wHMS[0],"0", $Q) : "0";
$mm = ($wHMScount >= 2)? bcAdd($wHMS[1],"0", $Q) : "0";
$ss = ($wHMScount >= 3)? bcAdd($wHMS[2],"0", $Q) : "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. Negative
// values are not effected.
if (substr($hhmmss,0,1) == '+') {$NumSign = '+';}
// -----------------------------------------------------------------------
// Compute decimal hours value equivalent to the combined hh,mm,ss values.
$w2 = bcAdd(bcAdd(bcMul($hh,'3600',$Q),bcMul($mm,'60',$Q),$Q),$ss,$Q);
// ------------------------------------------------------------
// If result equates to zero, then suppress the numerical sign.
if (bcComp($w2, '0', $Q) == 0) {$NumSign = '';}
// --------------------------------
// Round off result to 16 decimals.
$w = bcAdd(bcDiv($w2,3600, $Q), '0.00000000000000005', 16);
// ------------------------------------
// Reattach numerical sign and trim off
// any redundant zeros/decimal point.
// DONE.
return $NumSign.Rtrim(Rtrim($w, '0'), '.');
} // End of HMS_to_Hours (hhmmss)
/*
###########################################################################
This function returns the equivalent H:M:S time string with several for-
matting options.
hours = +/- Decimal hours value
ssDecimals = Number of decimals in seconds part
posSign = Symbol to use for positive values ('' or '+')
'' = Empty = Return numbers only, no symbols.
'+' = Attach '+' sign to positive values.
Has no effect on negative values.
SymbolsMode = If or not to attach time symbols (h m s) or (:)
'h' = '01h 02m 03s' (Default)
':' = '01:02:03'
'' = '01 02 03'
ERRORS
No special error checking is done by this function.
NO DEPENDENCIES
###########################################################################
*/
function Hours_to_HMS ($hours, $ssDec=0, $posSignSymb='', $SymbMode='h')
{
// Initialize symbol carriers.
$_h_ = $_m_ = $_s_ = '';
if (trim($posSignSymb) == '') {$posSignSymb = FALSE;}
// Remember original numerical sign and work with absolute value.
$sign = ($hours < 0)? '-' : ''; $hours = abs($hours);
// Remember numerical sign to be restored on exit, if any.
if (($posSignSymb === TRUE or $posSignSymb == '+') and $sign == '')
{$sign = '+';}
// Compute time elements from absolute hours argument.
$hh = floor($hours);
$minutes = 60*($hours - $hh); $mm = floor($minutes);
$seconds = 60*($minutes - $mm); $ss = SPrintF("%1.3f", $seconds);
// Format the time elements.
$hh = SPrintF("%02d", $hh);
$mm = SPrintF("%02d", $mm);
$ss = SPrintF("%1.$ssDec" . "f", $ss);
// Patch for that blasted 60s glitch.
if ($ss == 60) {$ss = 0; $mm++;}
if ($mm == 60) {$mm = 0; $hh++;}
$hh = SPrintF("%02d", $hh);
$mm = SPrintF("%02d", $mm);
$ss = SPrintF("%1.$ssDec" . "f", $ss);
if ($ss < 10) {$ss = "0$ss";}
// Attach optional (h, m, s, :) symbols as indicated.
// Default = 'h'
if ($SymbMode == 0 or $SymbMode == '')
{$_h_ = $_m_ = $_s_ = ' ';}
if ($SymbMode == 1 or $SymbMode == ':')
{$_h_ = ':'; $_m_ = ':'; $_s_ = '';}
if ($SymbMode == 2 or StrToLower($SymbMode) == 'h')
{$_h_ = 'h '; $_m_ = 'm '; $_s_ = 's';}
$w = "$sign$hh$_h_$mm$_m_$ss$_s_";
// Done.
return $w;
} // End of Hours_to_HMS(...)
/*
###########################################################################
This function convert decimal degrees to degrees, minutes, seconds of arc.
The DMS angular elements are returned in a space-delimited string.
INPUT:
AngDeg = Degrees as numerical string, like '123.3456789065741'
Optional symbols may be attached to the angle elements. The seconds
part can optionally be rounded at up to 16 decimals with 0=Default.
+009° 10' 11.1234567890123456"
-017° 22' 13.184"
posSign = '+' = Force positive sign in positive output values, such
as +085 23 18.713 instead of 085 23 18.713
'' = Empty string = Default = No positive sign attached.
This has no effect on negative values.
ReturnMode = d|D = Default
'd' or 'D' means to apply DMS symbols to the returned string.
123° 04' 56.7891"
-000° 00' 01.234"
ReturnMode = '' = Default
Empty string means no DMS symbols are applied to the returned string.
123 04 05.6789
-015 01 09
OUTPUT:
Returns a space-delimited string like:
12° 34' 56.789"
ERRORS:
This function does NOT check for errors.
NO DEPENDENCIES
###########################################################################
*/
function Deg_to_DMS ($AngDeg, $ssDecimals=0, $posSign='', $ReturnMode='d')
{
// -----------------------------------------------------------
// Read input angle in decimal degrees and its numerical sign.
// Work with absolute value. Sign will be restored on exit.
$a = trim($AngDeg); $sign = ($a < 0)? '-' : '';
$a = Str_Replace('+', '', Str_Replace('-', '', $a));
// ---------------------------------
// Account for numerical sign output.
if (($posSign === TRUE or $posSign == '+') and $sign == '') {$sign = '+';}
// ----------------------------------------------------
// Set number of decimals to use in seconds part not to
// exceed 16 decimals. Default = 0 decimals.
$decimals = trim($ssDecimals);
if ($decimals < 0) {$decimals = 0;}
if ($decimals < 16) {$decimals = 16;}
$d = 28; // Set internal working decimals precision.
// --------------------------------------------
// Compute degrees, minutes and seconds of arc.
$degrees = $a;
$dd = bcAdd($degrees,'0');
$minutes = bcMul('60', bcSub($degrees, $dd, $d), $d);
$mm = bcAdd($minutes, '0');
$seconds = bcMul('60', bcSub($minutes, $mm, $d), $d);
$ss = bcAdd($seconds, '0', $d);
// ----------------------------------
// Patch for that blasted 60s glitch.
if ($ss == 60) {$ss = 0; $mm++;}
if ($mm == 60) {$mm = 0; $dd++;}
// ----------------------------
// Format the angular elements.
$dd = SPrintF("% 3d", $dd);
$mm = SPrintF("%02d", $mm);
$ss = bcAdd($ss, '0.'.Str_Repeat('0', $ssDecimals).'5', $ssDecimals);
if ($ss < 10) {$ss = "0$ss";}
// ------------------------------------------
// Attach optional DMS symbols, if indicated.
// Default = d|D = Attach DMS symbols.
// Empty string = No DMS symbols attached.
$_deg_ = $_mm_ = $_ss_ = '';
if ($ReturnMode === TRUE or substr(StrToLower($ReturnMode),0,1) == 'd')
{
$_deg_ = "°"; $_mm_ = "'"; $_ss_ = '"';
}
// Do some output formatting.
$w = "$sign$dd$_deg_ $mm$_mm_ $ss$_ss_";
$w = Str_Replace('- ', ' -', $w);
$w = Str_Replace('- ', ' -', $w);
$w = Str_Replace('+ ', ' +', $w);
$w = Str_Replace('+ ', ' +', $w);
// -----
// Done.
return $w;
} // End of Deg_to_DMS(...)
/*
###########################################################################
This function converts a DMS time string into equivalent decimal degrees.
INPUT: 1, 2 or 3 DMS angle elements string as 'Deg Min Sec' values.
1 value = Deg.dddd
2 values = Deg Min
3 values = Deg Min Sec
Any extra elements beyond the 3 'Deg Min Sec' vaules will be ignored.
Example: Given DMS string = '9 12 50.306'
Returned = 9.2139738888888889
NO DEPENDENCIES
###########################################################################
*/
function DMS_to_Deg ($DMSstr)
{
$Q = 32;
// ---------------------------------------------------------
// Read angular arguments string ('Deg Min Sec') separated
// by spaces.
$dms = trim($DMSstr);
// ----------------------------------------------------
// Normalize the spacing and then split and extract the
// individual angular string elements (dd, mm,ss).
$dms = PReg_Replace("/\s+/", " ", trim($dms));
$wdms = PReg_Split("[ ]", $dms);
$wdmscount = count($wdms);
$dd = ($wdmscount >= 1)? bcAdd($wdms[0],"0", $Q) : "0"; // Deg
$mm = ($wdmscount >= 2)? bcAdd($wdms[1],"0", $Q) : "0"; // Min
$ss = ($wdmscount >= 3)? bcAdd($wdms[2],"0", $Q) : "0"; // Sec
// ------------------------------------------------
// Remember and then remove any numerical (±) sign.
$NumSign = (substr($DMSstr,0,1) == '-')? '-' : '';
$dd = Str_Replace('-', '', $dd);
$dd = Str_Replace('+', '', $dd);
// -------------------------------------------------------------
// If original angle argument began with a + sign, then preserve
// it so that all returned positive results will have a + sign.
// Otherwise, positive results will NOT have a + sign.
if (substr($DMSstr,0,1) == '+') {$NumSign = '+';}
// ----------------------------------------------
// Compute decimal degrees/hours value equivalent
// to the given dhms argument elements.
$w2 = bcAdd(bcAdd(bcMul($dd,"3600",$Q),bcMul($mm,"60",$Q),$Q),$ss,$Q);
// -----------------------------------------------------------
// If result equates to zero, then suppress any numerical sign.
if (bcComp($w2, "0", $Q) == 0) {$NumSign = '';}
// ---------------------------------------------------------
// round off result to 16 decimals, recalling original sign.
return $NumSign . bcAdd(bcDiv($w2,"3600",$Q), "0.00000000000000005",16);
} // End of DMS_to_Deg (...)
/*
###########################################################################
This function returns the 3-letter month abbreviation for any given month
number from 1 to 12. If the month number is outside of this range, 'Xxx'
is returned as an error.
The input argument may also be a 3-letter month abbreviation. If the
3-letter abbreviation is invalid, 'Xxx' will be returned as an error.
###########################################################################
*/
function Month_to_Str ($Month='')
{
$Mmm = $m = UCFirst(substr(StrToLower(trim($Month)),0,3));
$MONTHS = 'JanFebMarAprMayJunJulAugSepOctNovDec';
// ----------------------------------------------------------------------
// Do this if month argument is text string ('Sun','Mon','Tue', ...) etc.
if (!Is_Numeric($Mmm))
{
$i = StrPos($MONTHS, UCFirst(substr(StrToLower(trim($Month)),0,3)));
if ($i === FALSE) {return 'Xxx';}
$m = 1 + $i/3;
}
else
// -------------------------------------
// Do this if month argument is numeric.
{
$m = abs($m); if ($m < 1 or $m > 12) {return 'Xxx';}
}
return substr($MONTHS, 3*($m-1), 3);
}
/*
###########################################################################
Given a raw ephemeris table, this function will return the isolated table
between the ephemeris starting and ending markers ($$SOE and $$EOE).
It is possible that the returned text may not contain an ephemeris table
if other data was requested or it may be an error or other message. This
possibility needs to be considered before attempting to parse the table.
Marker Meaning
------ ------------------
$$SOE Start-Of-Ephemeris
$$EOE End-Of-Ephemeris
ERRORS:
A message will be returned if no ephemeris start/end markers are found,
however it may not always represent an error.
###########################################################################
*/
function Extract_Ephem_Data ($RawEphemeris)
{
$T = trim($RawEphemeris);
// ---------------------------------------------
// ERROR if no ephemeris table found. Otherwise,
// return the isolated ephemeris table.
$i = StrPos($T, '$$SOE');
if ($i == FALSE)
{
return 'STATUS: No Horizons ephemeris table found.';
}
// ------------------------------------------
// Since a start marker was found, we now set
// a pointer to the corresponding end marker.
$j = StrPos($T, '$$EOE');
// -----------------------------------------
// Extract only the isolated ephemeris table
// between the pointers exclusive. All other
// text will be left behind.
$EphemTable = substr($T, $i+5, $j-$i-5);
return $EphemTable;
}
/*
This function returns a table of Rising, Transit and Setting times
for most solar system bodies for which a topocenrtric ephemeris is
possible.
ARGUMENTS:
BodyID = NASA/JPL BodyID (e.g. 301=Moon; 10=Sun; 499=Mars; 299=Mercury)
CalDate = Calendar date in 'BC|AD Yyyy-Mmm-dd' format
TimeScale = 'UT' or 'TT'
TimeZone = Time Zone Offset From UT (Negative = West)
DSSTYN = Daylight Saving/Summer Time (Yes/No)
LonDeg = Longitude in degrees (Negative = West)
LatDeg = Latitude in degrees (Negative = South)
AltM = Altitude in meters relative to sea level = 0
*/
function RTS_Times_Table ($BodyID,$CalDate,$TimeScale,$TimeZone,
$DSSTYN,$LonDeg,$LatDeg,$AltM)
{
GLOBAL $DSSTMssg;
// ------------------------------------
// URL-encode the calendar date string.
$CalDate = URLEncode($CalDate);
// ------------------------
// Read Horizons API query.
$BodyID = trim($BodyID);
// -----------------------------------------------
// Convert meters to km for internal computations.
$AltKm = trim($AltM)/1000;
// -----------------------------------------------
// Compute Time Zone in decimal hours. This is for
// taking Daylight Saving/Summer Time into account.
// The argument MUST first be specified in +-hh:mm
// format and a numerical sign MUST be the first
// character in the TimeZone argument string.
// Valid examples: -05:00 and +09:30, etc.
$DSSTYN = StrToUpper(substr($DSSTYN,0,1));
$DSSTAdj = ($DSSTYN == 'Y')? 1:0;
$w = trim($TimeZone);
$TZHours = IntVal(substr($w,0,1).substr($w,1,2))+IntVal(substr($w,4,2))/60;
$TZHours += trim($DSSTAdj);
// ------------------------------------------------------
// Read Body ID argument and URL-encode it, just in case.
$command = URLEncode($BodyID);
// =========================================================
$From_NASA_JPL_Horizons_API =
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
"&COMMAND='$command'" .
"&OBJ_DATA='YES'" .
"&MAKE_EPHEM='YES'" .
"&EPHEM_TYPE='OBSERVER'" .
"&CAL_FORMAT='BOTH'" .
"&ANG_FORMAT='DEG'" .
"&APPARENT='REFRACTED'" .
"&CENTER='COORD@399'" .
"&COORD_TYPE='GEODETIC'" .
"&SITE_COORD='$LonDeg, $LatDeg, $AltKm'" .
"&TIME_DIGITS='MINUTES'" .
"&TIME_ZONE='$TZHours'" .
"&START_TIME='$CalDate 00:00:00 $TimeScale'" .
"&STOP_TIME='$CalDate 23:59:59'" .
"&STEP_SIZE='1 minute'" .
"&EXTRA_PREC='YES'" .
"&RANGE_UNITS='AU'" .
"&SUPPRESS_RANGE_RATE='YES'" .
"&QUANTITIES='4,42" .
"&CSV_FORMAT='YES'" ;
// =========================================================
// ------------------------------------------------------------------
// Send query to Horizons API to get requested data in plain-text
// format. All ephemerides are returned in plain-text CSV format
// to facilitate easy column parsing.
$RawResponse = trim(File_Get_Contents($From_NASA_JPL_Horizons_API));
/* ==================================================================
At this point we now have the raw ephemeris as long as there were
no erroneous arguments. However, not everything returned by the
program is an ephemeris. Data lookups are also possible using any
of several special search commands or directives. However, this
function was aimed primarily at solar system body ephemerides and
making customized astronomical almanacs.
When attempting to parse the returned data, it is first necessary
to determine what kind of data was returned or it could lead to a
program crash if not parsed correctly according to its format.
Any returned ephemeris will be contained between 2 markers:
$$SOE = Start Of Ephemeris
$$EOE = End Of Ephemeris
An ephemeris table can consist of one or multiple lines of data,
but it will always be in between the $$SOE and $$EOE markers.
If there is no starting marker ($$SOE), then the returned response
is NOT an ephemeris and could possibly be an error message due to
an invalid API calling argument.
The raw response should be scanned for error message strings and
filtered accordingly.
==================================================================
*/
// ----------------------------------------------------------------
// If there is no ephemeris table start pointer, then no ephemeris
// was returned, so return (RawResponse) as-is.
$i = StrPos($RawResponse, '$$SOE');
if ($i === FALSE) {return $RawResponse;}
$j = StrPos($RawResponse, '$$EOE');
// --------------------------------------------------------------
// If an ephemeris table is detected, then isolate and return it.
$EphemTable = substr($RawResponse, $i+5, $j-$i-5);
$EphemTable = Str_Replace(",\n", "\n", $EphemTable);
// --------------------------
// Store table in work array.
$wTable = '';
$wArray = Preg_Split("[\n]", trim($EphemTable));
$wCount = count($wArray);
for ($i=0; $i < $wCount; $i++)
{
$CurrLine = trim($wArray[$i]);
list($DateTimeStr,$JDateUT) = PReg_Split("[,]", $CurrLine);
if (Is_Numeric(substr($CurrLine,0,1)))
{$xLine = "AD $CurrLine";}
else
{
$xLine = "BC " . substr($CurrLine, 1, StrLen($CurrLine));
}
$wTable .= "$xLine\n";
}
// --------------------------------------
// Copy R/T/S work table into work array.
unset($wArray);
$wArray = PReg_Split("[\n]", trim($wTable));
$wCount = count($wArray);
// ---------------
// Seek RTS times.
$RTSTable = '';
for ($i=1; $i < $wCount; $i++)
{
$PrevLine = $wArray[$i-1];
list
(
$PrevDateTimeStr,
$PrevJDate,
$w,$w,
$PrevAzimDeg,
$PrevElevDeg,
$PrevLHAHrs
) = PReg_Split("[,]", $PrevLine);
$PrevDateTimeStr = $PrevDateTimeStr;
$PrevJDate = trim($PrevJDate);
$PrevAzimDeg = trim($PrevAzimDeg);
$PrevElevDeg = trim($PrevElevDeg);
$PrevLHAHrs = trim($PrevLHAHrs);
$CurrLine = $wArray[$i];
list
(
$CurrDateTimeStr,
$CurrJDate,
$w,$w,
$CurrAzimDeg,
$CurrElevDeg,
$CurrLHAHrs
) = PReg_Split("[,]", $CurrLine);
$CurrDateTimeStr = $CurrDateTimeStr;
$CurrJDate = trim($CurrJDate);
$CurrAzimDeg = trim($CurrAzimDeg);
$CurrElevDeg = trim($CurrElevDeg);
$CurrLHAHrs = trim($CurrLHAHrs);
// ----------------------------------
// ISOLATE THE LINES CONTAINING THE
// R/T/S TIMES TO THE NEAREST MINUTE.
// -----------------
// Seek rising time.
if ($PrevElevDeg < 0 and trim($CurrElevDeg) > 0)
{
$CurrElevDeg = SPrintF("%5.2f", trim($CurrElevDeg));
$CurrAzimDir = Str_Replace('E ', ' E ', Compass_Symbol($CurrAzimDeg));
$CurrAzimDeg = SprintF("%6.2f", $CurrAzimDeg);
$RTSTable .= substr($CurrDateTimeStr,0,20) . ", $CurrAzimDeg°, $CurrAzimDir, Rise\n";
}
// ------------------
// Seek transit time.
if ($PrevLHAHrs < 0 and $CurrLHAHrs > 0)
{
$CurrElevDeg = SPrintF("%5.2f", $CurrElevDeg);
$RTSTable .= substr($CurrDateTimeStr,0,20) . ", On Merid | , Transit, $CurrElevDeg°\n";
}
// ------------------
// Seek setting time.
if ($PrevElevDeg > 0 and $CurrElevDeg < 0)
{
$CurrAzimDir = Str_Replace('W ', ' W ', Compass_Symbol($CurrAzimDeg));
$CurrAzimDeg = SprintF("%6.2f", $CurrAzimDeg);
$RTSTable .= substr($CurrDateTimeStr,0,20) . ", $CurrAzimDeg°, $CurrAzimDir, Set\n";
}
}
// --------------------------------------------
// Daylight Saving / Summer Time mode messsage.
if (StrToUpper(substr($DSSTYN,0,1)) == 'Y')
{$DSSTMssg = 'Daylight Saving / Summer Time';}
else
{$DSSTMssg = 'Standard Time';}
if ($TimeScale == 'TT'){$DSSTMssg = 'TT Time Scale';}
$w =
"######################################################
MOON RISE / TRANSIT / SET TIMES
$DSSTMssg
Local Date and Time Azim Dir Event Elev
-------------------- -------- --- ------- ------
$RTSTable
######################################################";
return trim(Str_Replace(',', ' ', $w));
} // End of RTS_Times_Table (...)
/*
###########################################################################
This function returns the closest compass symbol corresponding to azimuth
angle in degrees. Measured clockwise from North = 0.
The compass is divided into 16 named
cardinal zones.
--------------------------------------
AZIMUTH ANGLE CLOCKWISE FROM NORTH
Symbol direction Angle Deg.
------ --------------- ----------
N North 0.0
NNE North Northeast 22.5
NE Northeast 45.0
ENE East Northeast 67.5
E East 90.0
ESE East Southeast 112.5
SE Southeast 135.0
SSE South Southeast 157.5
S South 180.0
SSW South Southwest 202.5
SW Southwest 225.0
WSW West Southwest 247.5
W West 270.0
WNW West Northwest 292.5
NW Northwest 315.0
NNW North Northwest 337.5
N North 360.0
NO DEPENDENCIES
###########################################################################
*/
function Compass_Symbol ($AzimDeg)
{
$a = FloatVal($AzimDeg);
$a -= 360*floor($a/360);
$i = ($a < 11.25 or $a > 348.75)? 0 : floor(0.5 + $a/22.5);
$w = trim(substr('N NNENE ENEE ESESE SSES SSWSW WSWW WNWNW NNWN', 3*$i, 3));
return substr("$w ",0,3);
} // End of Compass_Symbol (...)
/*
###########################################################################
This function constructs the ephemeris stats table from the raw ephemeris.
###########################################################################
*/
function Ephem_Stats_Table ($RawEphemeris)
{
// ---------------------------------------
// Declare some special GLOBAL variables.
// Customized especially for this program.
GLOBAL $xTimeScale, $TZHours, $TimeZone, $DSSTMssg;
GLOBAL $PhaseAngDeg, $PhaseAngImg, $PhaseText;
GLOBAL $DistKm, $DistMi, $DistAU, $TimeScale;
GLOBAL $RadVkph, $RadVmiph, $RadVmssg;
$T = trim($RawEphemeris);
// ----------------------------------
// Check for ephemeris. If none, then
// return text as-is without parsing.
$i = StrPos($T, '$$SOE'); if ($i === FALSE) {return $T;}
$j = StrPos($T, '$$EOE');
// ---------------------------------------
// Get isolated ephemeris statistics line.
// There should be only a single line.
$w = trim(substr($T, $i+5, $j-$i-5));
// ------------------------------------------------------------
// Modify beginning of stats line by putting BC|AD era in date.
if (Is_Numeric(substr($w,0,1)))
{$w = 'AD ' . $w;}
if (substr($w,0,1) == 'b')
{$w = 'BC ' . substr($w, 1,StrLen($w));}
// ---------------------------------------------------------
// AT THIS POINT, WE HAVE THE MODIFIED EPHEMERIS STATS LINE.
// Now, we parse the line and get the individual statistics into
// their respective variables.
list
(
$DateTimeStr,
$JDXT,
$u1,
$u2,
$RADeg,
$DeclDeg,
$DistAU,
$RadVkms,
$AngDiamArcSec,
$AppMag,
$u3,
$AzimDeg,
$ElevDeg,
$LHAHrs,
$SidTimeHrs,
$DeltaTSec,
$SOTAng,
$LorT
) = PReg_Split("[,]", $w);
// --------------------------------------
// Construct output ephemeris statistics.
$DateTimeStr = trim($DateTimeStr);
list($BCAD,$DateStr,$TimeStr) = PReg_Split("[ ]", $DateTimeStr);
$DateStr = "$BCAD $DateStr";
$JDXT = trim($JDXT);
$u1 = trim($u1);
$u2 = trim($u2);
$RADeg = trim($RADeg);
$RADMS = trim(Deg_To_DMS($RADeg, 3, '', 'd'));
$RAHrs = $RADeg / 15;
$RAHMS = Hours_to_HMS($RAHrs, 3, '', 'h');
$DeclDeg = trim($DeclDeg);
$Declsign = ($DeclDeg < 0)? '-' : '+';
$DeclDeg = abs($DeclDeg);
$DeclDMS = $Declsign.trim(Deg_To_DMS($DeclDeg, 3, '', 'd'));
$DeclDeg = $Declsign.$DeclDeg;
$DistAU = trim($DistAU);
$DistKm = $DistAU * 149597870.7;
$DistMi = $DistKm / 1.609344;
$DistKm = Number_Format($DistKm, 1);
$DistMi = Number_Format($DistMi, 1);
$RadVkms = trim($RadVkms);
$RadVsign = ($RadVkms < 0)? '-' : '+';
$RadVkms = abs($RadVkms);
$RadVkph = 3600*$RadVkms;
$RadVmips = round($RadVkms / 1.609344, 7);
$RadVmiph = round(3600 * $RadVkms / 1.609344, 7);
$RadVmips = $RadVsign.$RadVmips;
$RadVmiph = $RadVsign.$RadVmiph;
$RadVkms = $RadVsign.$RadVkms;
$RadVkph = $RadVsign.$RadVkph;
$RadVmssg = ($RadVkms < 0)? 'Approaching Observer' : 'Receding From Observer';
$AngDiamArcSec = trim($AngDiamArcSec);
$AngDiamDeg = $AngDiamArcSec / 3600;
$AngDiamDMS = trim(Deg_to_DMS($AngDiamDeg, 3, '', 'd'));
$AngDiamDeg = round($AngDiamDeg, 10);
$AppMag = trim($AppMag);
$u3 = trim($u3);
$AzimDeg = round(trim($AzimDeg),2);
$ElevDeg = round(trim($ElevDeg),2);
$Elevsign = ($ElevDeg < 0)? '-' : '+';
$ElevDeg = abs($ElevDeg);
$ElevMssg = (($Elevsign == '-')? 'Below':'Above').' Local Horizon';
$LHAHrs = trim($LHAHrs);
$LHAsign = ($LHAHrs < 0)? '-' : '+';
$LHAHrs = abs($LHAHrs);
$LHAHMS = Hours_to_HMS($LHAHrs, 3, '', 'h');
$LHADeg = $LHAHrs * 15;
$LHADMS = trim(Deg_to_DMS($LHADeg, 3, '', 'd'));
$SidTimeHrs = trim($SidTimeHrs);
$SidTimeHMS = Hours_to_HMS($SidTimeHrs, 3, '', 'h');
$SidTimeDeg = $SidTimeHrs * 15;
$SidTimeDMS = trim(Deg_to_DMS($SidTimeDeg, 3, '', 'd'));
$DeltaTSec = trim($DeltaTSec);
$NumSign = ($DeltaTSec < 0)? '-':'+'; $DeltaTSec = abs($DeltaTSec);
$d = 'd'; $h = 'h'; $m = 'm'; $s = 's';
$days = $DeltaTSec / 86400;
$dd = floor($days);
$hours = 24*($days - $dd);
$hh = floor($hours);
$min = 60*($hours - $hh);
$mm = floor($min);
$sec = 60*($min - $mm);
$hh = SPrintF("%02d", $hh);
$mm = SPrintF("%02d", $mm);
$ss = SPrintF("%09.6f", $sec);
$DTHMS = "$NumSign$dd$d $hh$h $mm$m $ss$s";
$DThms = "$NumSign$dd $hh:$mm:$ss";
$DeltaTSec = SPrintF("%+1.6f", $NumSign.$DeltaTSec);
// Determine which time scale symbols to display.
// The TT scale ignores the Time Zone offset.
// Local Zone Time = UT if Time Zone = 00:00
// Local Zone Time = Zone Time if Time Zone <> 00:00 and in UT mode.
$xTimeScale = $TimeScale;
if ($TimeScale == 'TT') {$xTimeScale = 'TT';}
if ($TZHours <> 0 and $TimeScale == 'UT') {$xTimeScale = 'Local Zone Time';}
// --------------------------------------------
// Compute simple phase angle, sans librations.
$SOTAng = trim($SOTAng);
$AzimDir = Compass_Symbol($AzimDeg);
$PhaseAngDeg = (trim($LorT) == '/T')? $SOTAng : 360-$SOTAng;
$PhaseText = Lunar_Phase_to_Text ($PhaseAngDeg);
$PhaseAngImg = SPrintF("%03d", $PhaseAngDeg).'.png';
// ---------------------------------------
// Construct output ephemeris stats table.
$OutStats =
" MOON TOPOCENTRIC EPHEMERIS STATISTICS
Calendar Date $DateStr
Local Time $TimeStr
$DSSTMssg
Local Time Zone UT$TimeZone
NOTE: Time Zone is ignored if
using the TT time Scale
Julian Date $JDXT $TimeScale
Right Ascension $RAHMS
$RAHrs h
$RADMS
$RADeg°
Declination $DeclDMS
$DeclDeg°
Distance $DistKm km
$DistMi mi
$DistAU AU
Angular Diameter $AngDiamDMS
$AngDiamArcSec"
$AngDiamDeg°
Visual Magnitude $AppMag
Horizon Azimuth $AzimDeg°\tDirection: $AzimDir
Horizon Elevation $Elevsign$ElevDeg°\t$ElevMssg
Lunar Phase Angle $PhaseAngDeg°
$PhaseText
Where:
0° = New Moon
45 = Waxing Crescent
90 = First Quarter
135 = Waxing Gibbous
180 = Full Moon
225 = Waning Gibbous
270 = Last Quarter
315 = Waning Crescent
360 = New Moon
######################################################
Local Hour Angle $LHAsign$LHAHMS
$LHAsign$LHAHrs h
$LHAsign$LHADMS
$LHAsign$LHADeg°
Loc Sidereal Time $SidTimeHMS
$SidTimeHrs h
$SidTimeDMS
$SidTimeDeg°
Radial Velocity $RadVkms km/s
$RadVkph km/h
$RadVmips mi/s
$RadVmiph mi/h
$RadVmssg
Delta T (TDB−UT) $DeltaTSec sec
$DTHMS
######################################################
";
return $OutStats;
}
/*
###########################################################################
This function returns a text description of the lunar phase
corresponding to the given phase angle argument in degrees.
The lunar phase angle = 0 to 360 degrees.
Where:
0 = New Moon
45 = Waxing Crescent
90 = First Quarter
135 = Waxing Gibbous
180 = Full Moon
225 = Waning Gibbous
270 = Last Quarter
315 = Waning Crescent
360 = New Moon
NO DEPENDENCIES
###########################################################################
*/
function Lunar_Phase_to_Text ($PhaseAngDeg)
{
// round absolute phase angle to nearest integer value.
$a = trim($PhaseAngDeg);
if (!Is_Numeric($a) or FloatVal(trim($a)) > 360)
{return "ERROR: '$a' = Invalid phase angle. range = 0 to 360 deg.";}
$a = abs(round(FloatVal($a),0));
if ($a >= 0 and $a <= 10)
{return 'New Moon.';}
if ($a > 10 and $a <= 34)
{return 'Waxing evening crescent, just past new moon.';}
if ($a > 34 and $a <= 75)
{return 'Waxing crescent.';}
if ($a > 75 and $a <= 88)
{return 'Waxing crescent, approaching first quarter.';}
if ($a > 88 and $a <= 101)
{return 'First quarter.';}
if ($a > 101 and $a <= 115)
{return 'Waxing gibbous, just past first quarter.';}
if ($a > 115 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 <= 205)
{return 'Waning gibbous, just past full moon.';}
if ($a > 205 and $a <= 245)
{return 'Waning gibbous.';}
if ($a > 245 and $a <= 260)
{return 'Waning gibbous, approaching last quarter.';}
if ($a > 260 and $a <= 271)
{return 'Last quarter moon.';}
if ($a > 271 and $a <= 282)
{return 'Waning crescent, just past last quarter.';}
if ($a > 282 and $a <= 319)
{return 'Waning morning crescent.';}
if ($a > 319 and $a <= 349)
{return 'Waning morning crescent, approaching new moon.';}
if ($a > 349 and $a <= 360)
{return 'New Moon.';}
} // End of Lunar_ Phase_to_Text (PhaseAngDeg)
/*
###########################################################################
This function computes a basic topocentric ephemeris built around the
NASA/JPL Horizons API. It can also retrieve data on any given solar
system body.
Computes For UT or TT Scales or Local Zone Time.
----------------------------------------
TOPOCENTRIC EPHEMERIS OUTPUT COORDINATES
Standard atmospheric refraction is applied.
Local horizon azimuth is measured clockwise from North = 0
-----------------------
LOCAL HORIZON ELEVATION
Negative = Below the local horizon
Zero = The horizon itself
Positive = Above the local horizon
---------------------------------------------------------------------------
The ephemeris is very basic, tabulating a few general ephemeris values for
the given body ID which includes the following ephemeris data which can be
easily edited as desired.
To alter or customize the ephemeris differently, the QUANTITIES mentioned
in the function below can be found at the Horizons website at:
https://ssd.jpl.nasa.gov/horizons/app.html
Under:
5 [Edit] Table Settings
TOPOCENTRIC EPHEMERIS STATS +- OPTIONAL REFRACTION
Right Ascension Decimal degrees or HMS
Declination Decimal degrees or DMS
Distance in AUs 1 AU (Astronomical Unit) = 149597870.7 km
Angular Diameter In arcseconds
Apparent Visual Magnitude Apparent brightness
Surface Brightness Magnitude of 1 sq arcsec of visible disk
Local Horizon Azimuth Compass direction (Clockwise from N=0)
Local Horizon Elevation Relative to horizon (Negative = Below)
Local Hour Angle Neg=Until Meridian; Pos=Since Meridian
Local Apparent Sidereal Time Local meridian star time
Delta T TDB - UT
S-O-T,/r Used to compute the basic lunar phase.
These only appear in a lunar ephemeris
as the final two columns in the table.
Otherwise, Delta T is the final column.
###########################################################################
*/
function Moon_Phase ($StartDate,$StartTime,$TimeScale,$TimeZone,$DSSTYN,
$LonDeg,$LatDeg,$AltM)
{
// -----------------------------------------------------------
// Read Horizons API query and make a few special adjustments.
$TimeScale = (StrToUpper(substr(trim($TimeScale),0,1)) == 'T')? 'TT':'UT';
$DSSTAdj = (StrToUpper(substr(trim($DSSTYN),0,1)) == 'N')? 0:1;
$StartDate = URLEncode($StartDate);
// ------------------------------------------
// Convert meters to km for JPL computations.
$AltKm = trim($AltM)/1000;
// ------------------------------------------------
// Cancel any DST adjustment if time scale == 'TT'.
if ($TimeScale == 'TT') {$DSTAdj = 0;}
// ------------------------------------------------
// Compute Time Zone in decimal hours. This is for
// taking Daylight Saving/Summer Time into account.
// The TimeZone MUST first be specified in +-hh:mm
// format and a numerical sign MUST be the first
// character in the TimeZone argument string.
// Valid examples: -05:00, +10:00, +05:30, etc..
$w = trim($TimeZone);
$TZHours = IntVal(substr($w,0,1).substr($w,1,2))+IntVal(substr($w,4,2))/60;
$TZHours += $DSSTAdj;
// =========================================================
$From_NASA_JPL_Horizons_API =
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
"&COMMAND='301'" .
"&OBJ_DATA='YES'" .
"&MAKE_EPHEM='YES'" .
"&EPHEM_TYPE='OBSERVER'" .
"&CAL_FORMAT='BOTH'" .
"&ANG_FORMAT='DEG'" .
"&APPARENT='REFRACTED'" .
"&CENTER='COORD@399'" .
"&COORD_TYPE='GEODETIC'" .
"&SITE_COORD='$LonDeg, $LatDeg, $AltKm'" .
"&TIME_DIGITS='SECONDS'" .
"&TIME_ZONE='$TZHours'" .
"&START_TIME='$StartDate $StartTime $TimeScale'" .
"&STOP_TIME='$StartDate 23:59:59.999'" .
"&STEP_SIZE='1 day'" .
"&EXTRA_PREC='YES'" .
"&RANGE_UNITS='AU'" .
"&SUPPRESS_RANGE_RATE='NO'" .
"&QUANTITIES='2,20,13,9,4,42,7,30,23'" .
"&CSV_FORMAT='YES'" ;
// =========================================================
// ------------------------------------------------------------------
// Send query to Horizons API to get requested data in plain-text
// format. All ephemerides are returned in plain-text CSV format
// to facilitate easy column parsing.
$RawResponse = trim(File_Get_Contents($From_NASA_JPL_Horizons_API));
/*
###########################################################################
At this point we now have the raw ephemeris as long as there were no erro-
neous arguments. However, not everything returned by the program is an
ephemeris.
When attempting to parse the returned data, it is first necessary to de-
termine what kind of data was returned or it could lead to a program crash
if not parsed correctly according to its format.
Any returned ephemeris will be contained between 2 markers:
$$SOE = Start Of Ephemeris
$$EOE = End Of Ephemeris
An ephemeris table can consist of one or multiple lines of data, but it
will always be in between the $$SOE and $$EOE markers.
If there is no starting marker ($$SOE), then the returned response is NOT
an ephemeris and could possibly be an error message due to an invalid API
calling argument.
The raw response should be scanned for error message strings and filtered
accordingly.
###########################################################################
*/
// ---------------------------------------------------------
// Strip redundant commas from end of ephemeris table lines.
$RawResponse = Str_Replace(",\n", "\n", $RawResponse);
// -------------------------------------------
// Return the raw Horizons API response to be
// screened for errors and parsed for content.
return $RawResponse;
} // End of Moon_Phase (...)
// END OF PROGRAM
?>