Last active
February 6, 2024 16:08
-
-
Save themorgantown/1f01fc2bc90c01a64c73dcc9f0b6c259 to your computer and use it in GitHub Desktop.
Show an emoji of the Moon based on the current date (works well with Sendy on a PHP server). An example of using this in a sendy template: `https://example.com/moonphase/moonphase/[currentmonthnumber]-[currentdaynumber]-[currentyear].png` Requires png files in the moon_phases directory with these filenames: waning_crescent.png first_quarter.png …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# place this in the folder 'moonphase' at the top level directory to load | |
# https://example.com/moonphase/moonphase/02-03-2024.png | |
RewriteEngine On | |
# Check if mod_rewrite is enabled | |
<IfModule mod_rewrite.c> | |
# Rewrite only if the request is for a moon phase image | |
RewriteRule ^moonphase/([0-9]{2})-([0-9]{2})-([0-9]{4})\.png$ moonphase.php?month=$1&day=$2&year=$3 [L,QSA] | |
</IfModule> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// lordy https://aa.quae.nl/en/reken/hemelpositie.html | |
// Centralized response handling | |
function sendHttpResponse($code, $message = '', $contentType = 'text/plain') { | |
header("HTTP/1.1 $code"); | |
header("Content-Type: $contentType"); | |
if (!empty($message)) { | |
echo $message; | |
} | |
exit; | |
} | |
// Error logging function | |
function logError($message) { | |
// Assuming a writable directory 'logs' exists. Adjust the path as needed. | |
error_log($message . "\n", 3, __DIR__ . '/logs/error_log.txt'); | |
} | |
require_once 'suncalc.php'; | |
use AurorasLive\SunCalc; | |
function getMoonPhaseImage($year, $month, $day) { | |
$dateString = "$year-$month-$day"; | |
$date = new DateTime($dateString); | |
// Specify your latitude and longitude values | |
$lat = 40.821287; // Example latitude | |
$lng = -73.923168; // Example longitude | |
// Create an instance of SunCalc with the date, latitude, and longitude | |
$sunCalc = new SunCalc($date, $lat, $lng); | |
// Calculate the moon illumination for the given date | |
$moonIllumination = $sunCalc->getMoonIllumination(); | |
// Determine the moon phase based on the phase value | |
$phase = $moonIllumination['phase']; | |
if ($phase < 0.03) { | |
return 'new_moon.png'; | |
} elseif ($phase < 0.22) { | |
return 'waxing_crescent.png'; | |
} elseif ($phase < 0.28) { | |
return 'first_quarter.png'; | |
} elseif ($phase < 0.47) { | |
return 'waxing_gibbous.png'; | |
} elseif ($phase < 0.53) { | |
return 'full_moon.png'; | |
} elseif ($phase < 0.72) { | |
return 'waning_gibbous.png'; | |
} elseif ($phase < 0.78) { | |
return 'last_quarter.png'; | |
} elseif ($phase < 0.97) { | |
return 'waning_crescent.png'; | |
} else { | |
return 'new_moon.png'; | |
} | |
} | |
$month = $_GET['month'] ?? null; | |
$day = $_GET['day'] ?? null; | |
$year = $_GET['year'] ?? null; | |
// More robust date validation | |
if (!preg_match('/^\d{2}-\d{2}-\d{4}$/', "$month-$day-$year") || !checkdate($month, $day, $year)) { | |
sendHttpResponse(400, 'Invalid date provided.'); | |
} | |
$moonPhaseImage = getMoonPhaseImage($year, $month, $day); | |
$imagePath = __DIR__ . '/moon_phases/' . $moonPhaseImage; | |
if (file_exists($imagePath)) { | |
header('Content-Type: image/png'); | |
// Set cache control: max-age is in seconds (example here is for one year) | |
header('Cache-Control: public, max-age=31536000'); | |
// Set Expires header for 1 year in the future | |
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); | |
readfile($imagePath); | |
} else { | |
logError('Moon phase image not found for date: ' . "$year-$month-$day"); | |
sendHttpResponse(404, 'Moon phase image not found.'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace AurorasLive; | |
use DateInterval; | |
/* | |
SunCalc is a PHP library for calculating sun/moon position and light phases. | |
https://github.com/gregseth/suncalc-php | |
Based on Vladimir Agafonkin's JavaScript library. | |
https://github.com/mourner/suncalc | |
Sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html | |
formulas. | |
Moon calculations are based on http://aa.quae.nl/en/reken/hemelpositie.html | |
formulas. | |
Calculations for illumination parameters of the moon are based on | |
http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and Chapter 48 | |
of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, | |
Richmond) 1998. | |
Calculations for moon rise/set times are based on | |
http://www.stargazing.net/kepler/moonrise.html article. | |
*/ | |
// shortcuts for easier to read formulas | |
define('PI', M_PI); | |
define('rad', PI / 180); | |
// date/time constants and conversions | |
define('daySec', 60 * 60 * 24); | |
define('J1970', 2440588); | |
define('J2000', 2451545); | |
// general calculations for position | |
define('e', rad * 23.4397); // obliquity of the Earth | |
define('J0', 0.0009); | |
function toJulian($date) { return $date->getTimestamp() / daySec - 0.5 + J1970; } | |
function fromJulian($j, $d) { | |
if (!is_nan($j)) { | |
$dt = new \DateTime("@".round(($j + 0.5 - J1970) * daySec)); | |
$dt->setTimezone($d->getTimezone()); | |
return $dt; | |
} | |
} | |
function toDays($date) { return toJulian($date) - J2000; } | |
function rightAscension($l, $b) { return atan2(sin($l) * cos(e) - tan($b) * sin(e), cos($l)); } | |
function declination($l, $b) { return asin(sin($b) * cos(e) + cos($b) * sin(e) * sin($l)); } | |
function azimuth($H, $phi, $dec) { return atan2(sin($H), cos($H) * sin($phi) - tan($dec) * cos($phi)); } | |
function altitude($H, $phi, $dec) { return asin(sin($phi) * sin($dec) + cos($phi) * cos($dec) * cos($H)); } | |
function siderealTime($d, $lw) { return rad * (280.16 + 360.9856235 * $d) - $lw; } | |
// calculations for sun times | |
function julianCycle($d, $lw) { return round($d - J0 - $lw / (2 * PI)); } | |
function approxTransit($Ht, $lw, $n) { return J0 + ($Ht + $lw) / (2 * PI) + $n; } | |
function solarTransitJ($ds, $M, $L) { return J2000 + $ds + 0.0053 * sin($M) - 0.0069 * sin(2 * $L); } | |
function hourAngle($h, $phi, $d) { return acos((sin($h) - sin($phi) * sin($d)) / (cos($phi) * cos($d))); } | |
// returns set time for the given sun altitude | |
function getSetJ($h, $lw, $phi, $dec, $n, $M, $L) { | |
$w = hourAngle($h, $phi, $dec); | |
$a = approxTransit($w, $lw, $n); | |
return solarTransitJ($a, $M, $L); | |
} | |
// general sun calculations | |
function solarMeanAnomaly($d) { return rad * (357.5291 + 0.98560028 * $d); } | |
function eclipticLongitude($M) { | |
$C = rad * (1.9148 * sin($M) + 0.02 * sin(2 * $M) + 0.0003 * sin(3 * $M)); // equation of center | |
$P = rad * 102.9372; // perihelion of the Earth | |
return $M + $C + $P + PI; | |
} | |
function hoursLater($date, $h) { | |
$dt = clone $date; | |
return $dt->add( new DateInterval('PT'.round($h*3600).'S') ); | |
} | |
class DecRa { | |
public $dec; | |
public $ra; | |
function __construct($d, $r) { | |
$this->dec = $d; | |
$this->ra = $r; | |
} | |
} | |
class DecRaDist extends DecRa { | |
public $dist; | |
function __construct($d, $r, $dist) { | |
parent::__construct($d, $r); | |
$this->dist = $dist; | |
} | |
} | |
class AzAlt { | |
public $azimuth; | |
public $altitude; | |
function __construct($az, $alt) { | |
$this->azimuth = $az; | |
$this->altitude = $alt; | |
} | |
} | |
class AzAltDist extends AzAlt { | |
public $dist; | |
function __construct($az, $alt, $dist) { | |
parent::__construct($az, $alt); | |
$this->dist = $dist; | |
} | |
} | |
function sunCoords($d) { | |
$M = solarMeanAnomaly($d); | |
$L = eclipticLongitude($M); | |
return new DecRa( | |
declination($L, 0), | |
rightAscension($L, 0) | |
); | |
} | |
function moonCoords($d) { // geocentric ecliptic coordinates of the moon | |
$L = rad * (218.316 + 13.176396 * $d); // ecliptic longitude | |
$M = rad * (134.963 + 13.064993 * $d); // mean anomaly | |
$F = rad * (93.272 + 13.229350 * $d); // mean distance | |
$l = $L + rad * 6.289 * sin($M); // longitude | |
$b = rad * 5.128 * sin($F); // latitude | |
$dt = 385001 - 20905 * cos($M); // distance to the moon in km | |
return new DecRaDist( | |
declination($l, $b), | |
rightAscension($l, $b), | |
$dt | |
); | |
} | |
class SunCalc { | |
var $date; | |
var $lat; | |
var $lng; | |
// sun times configuration (angle, morning name, evening name) | |
private $times = [ | |
[-0.833, 'sunrise', 'sunset' ], | |
[ -0.3, 'sunriseEnd', 'sunsetStart' ], | |
[ -6, 'dawn', 'dusk' ], | |
[ -12, 'nauticalDawn', 'nauticalDusk'], | |
[ -18, 'nightEnd', 'night' ], | |
[ 6, 'goldenHourEnd', 'goldenHour' ] | |
]; | |
// adds a custom time to the times config | |
private function addTime($angle, $riseName, $setName) { | |
$this->times[] = [$angle, $riseName, $setName]; | |
} | |
function __construct($date, $lat, $lng) { | |
$this->date = $date; | |
$this->lat = $lat; | |
$this->lng = $lng; | |
} | |
// calculates sun position for a given date and latitude/longitude | |
function getSunPosition() { | |
$lw = rad * -$this->lng; | |
$phi = rad * $this->lat; | |
$d = toDays($this->date); | |
$c = sunCoords($d); | |
$H = siderealTime($d, $lw) - $c->ra; | |
return new AzAlt( | |
azimuth($H, $phi, $c->dec), | |
altitude($H, $phi, $c->dec) | |
); | |
} | |
// calculates sun times for a given date and latitude/longitude | |
function getSunTimes() { | |
$lw = rad * -$this->lng; | |
$phi = rad * $this->lat; | |
$d = toDays($this->date); | |
$n = julianCycle($d, $lw); | |
$ds = approxTransit(0, $lw, $n); | |
$M = solarMeanAnomaly($ds); | |
$L = eclipticLongitude($M); | |
$dec = declination($L, 0); | |
$Jnoon = solarTransitJ($ds, $M, $L); | |
$result = [ | |
'solarNoon'=> fromJulian($Jnoon, $this->date), | |
'nadir' => fromJulian($Jnoon - 0.5, $this->date) | |
]; | |
for ($i = 0, $len = count($this->times); $i < $len; $i += 1) { | |
$time = $this->times[$i]; | |
$Jset = getSetJ($time[0] * rad, $lw, $phi, $dec, $n, $M, $L); | |
$Jrise = $Jnoon - ($Jset - $Jnoon); | |
$result[$time[1]] = fromJulian($Jrise, $this->date); | |
$result[$time[2]] = fromJulian($Jset, $this->date); | |
} | |
return $result; | |
} | |
function getMoonPosition($date) { | |
$lw = rad * -$this->lng; | |
$phi = rad * $this->lat; | |
$d = toDays($date); | |
$c = moonCoords($d); | |
$H = siderealTime($d, $lw) - $c->ra; | |
$h = altitude($H, $phi, $c->dec); | |
// altitude correction for refraction | |
$h = $h + rad * 0.017 / tan($h + rad * 10.26 / ($h + rad * 5.10)); | |
return new AzAltDist( | |
azimuth($H, $phi, $c->dec), | |
$h, | |
$c->dist | |
); | |
} | |
function getMoonIllumination() { | |
$d = toDays($this->date); | |
$s = sunCoords($d); | |
$m = moonCoords($d); | |
$sdist = 149598000; // distance from Earth to Sun in km | |
$phi = acos(sin($s->dec) * sin($m->dec) + cos($s->dec) * cos($m->dec) * cos($s->ra - $m->ra)); | |
$inc = atan2($sdist * sin($phi), $m->dist - $sdist * cos($phi)); | |
$angle = atan2(cos($s->dec) * sin($s->ra - $m->ra), sin($s->dec) * cos($m->dec) - cos($s->dec) * sin($m->dec) * cos($s->ra - $m->ra)); | |
return [ | |
'fraction' => (1 + cos($inc)) / 2, | |
'phase' => 0.5 + 0.5 * $inc * ($angle < 0 ? -1 : 1) / PI, | |
'angle' => $angle | |
]; | |
} | |
function getMoonTimes($inUTC=false) { | |
$t = clone $this->date; | |
if ($inUTC) $t->setTimezone(new \DateTimeZone('UTC')); | |
$t->setTime(0, 0, 0); | |
$hc = 0.133 * rad; | |
$h0 = $this->getMoonPosition($t, $this->lat, $this->lng)->altitude - $hc; | |
$rise = 0; | |
$set = 0; | |
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) | |
for ($i = 1; $i <= 24; $i += 2) { | |
$h1 = $this->getMoonPosition(hoursLater($t, $i), $this->lat, $this->lng)->altitude - $hc; | |
$h2 = $this->getMoonPosition(hoursLater($t, $i + 1), $this->lat, $this->lng)->altitude - $hc; | |
$a = ($h0 + $h2) / 2 - $h1; | |
$b = ($h2 - $h0) / 2; | |
$xe = -$b / (2 * $a); | |
$ye = ($a * $xe + $b) * $xe + $h1; | |
$d = $b * $b - 4 * $a * $h1; | |
$roots = 0; | |
if ($d >= 0) { | |
$dx = sqrt($d) / (abs($a) * 2); | |
$x1 = $xe - $dx; | |
$x2 = $xe + $dx; | |
if (abs($x1) <= 1) $roots++; | |
if (abs($x2) <= 1) $roots++; | |
if ($x1 < -1) $x1 = $x2; | |
} | |
if ($roots === 1) { | |
if ($h0 < 0) $rise = $i + $x1; | |
else $set = $i + $x1; | |
} else if ($roots === 2) { | |
$rise = $i + ($ye < 0 ? $x2 : $x1); | |
$set = $i + ($ye < 0 ? $x1 : $x2); | |
} | |
if ($rise != 0 && $set != 0) break; | |
$h0 = $h2; | |
} | |
$result = []; | |
if ($rise != 0) $result['moonrise'] = hoursLater($t, $rise); | |
if ($set != 0) $result['moonset'] = hoursLater($t, $set); | |
if ($rise==0 && $set==0) $result[$ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; | |
return $result; | |
} | |
} | |
// tests | |
/* | |
$test = new SunCalc(new \DateTime(), 48.85, 2.35); | |
print_r($test->getSunTimes()); | |
print_r($test->getMoonIllumination()); | |
print_r($test->getMoonTimes()); | |
print_r(getMoonPosition(new \DateTime(), 48.85, 2.35)); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment