Inleiding
Sinds het ontwikkelen van de persoonlijke weekkalender in PHP maak ik daar elk jaar dankbaar gebruik van. Een druk op de knop en een PDF-document met voor alle weken de (eventueel persoonlijke) feestdagen wordt gedownload. Uitprinten en perforeren, knutselen op een kartonnen achterkant; klaar.
Nu zag ik dezer dagen op een andere kalender bij sommige dagen kleine symbolen staan: rondjes, soms wit, soms zwart. Soms half gevuld, soms gespiegeld. Het waren de fasen van de maan op die specifieke dagen. Nieuwe maan, eerste kwartier, volle maan, laatste kwartier. Ik dacht: dat zou een leuke toevoeging zijn voor mijn eigen kalender!
Onderzoek
Behalve de technische uitdaging van dit project, heb ik niets met de maanfases en wat die voor de mens zouden kunnen betekenen. In de astrologie speelt de maan (Luna) een grote rol. In de astronomie vind ik vooral de kosmologische geschiedenis en bijbehorende natuurkunde heel interessant (met grote dank aan Prof. Dr. Harald Lesch).
Aldus zocht en vond ik:
- php-moon-phase, een PHP project dat de maanfase op een bepaalde datum-tijd kan berekenen;
- een online bron met maanfases voor het huidige en komende jaar, om mijn resultaten te kunnen controleren.
Unicode of niet?
Uiteindelijk zou er in het met mPDF gegenereerde PDF-document een maan-symbool moeten verschijnen op de betreffende datum. Mijn eerste gedachte: in Unicode zouden de passende symbolen beslist zijn opgenomen. En zie daar: 🌑 (nieuwe maan), 🌓 (eerste kwartier), 🌗 (laatste kwartier) en 🌕 (volle maan).
Jammer genoeg verschenen er bij de eerste test alleen maar blokjes in het PDF-document. De ingebakken lettertypes (embedded fonts) boden die Unicode symbolen blijkbaar niet. Ik moest op zoek naar een andere oplossing.
TTF-maan
mPDF heeft de mogelijkheid om extra lettertypen toe te voegen. Op dafont.com vond ik een lettertype van Curtis Clark (1995) met elke maanfase als apart symbool. Met de volgende code wordt het TTF-bestand toegevoegd:
use \Mpdf\Config\ConfigVariables;
use \Mpdf\Config\FontVariables;
use \Mpdf\Mpdf;
$defaultConfig = (new ConfigVariables())->getDefaults();
$fontDirs = $defaultConfig['fontDir'];
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontData = $defaultFontConfig['fontdata'];
$pdfConfig = [
...
'fontDir' => array_merge(
$fontDirs, [
__DIR__ . '/../fonts',
]
),
'fontdata' => $fontData + [
'moonphases' => [
'R' => 'moonphases.ttf',
]
],
...
];
$pdf = new Mpdf($pdfConfig);
Verderop in het schrijven van de HTML (die wordt omgezet naar PDF) kan
met behulp van de CSS font-family-aanwijzing de betreffende symbolen
worden geplaatst. Het @-symbool staat voor volle maan:
<span style="font-family:moonphases">@</span>
De koppeling tussen de maanfases van \Solaris\MoonPhase en de
specifieke letters van het TTF-font was even handwerk:
// Symbols
[
MoonPhaseName::NEW_MOON => '0',
MoonPhaseName::WAXING_CRESCENT => 'D',
MoonPhaseName::FIRST_QUARTER => 'G',
MoonPhaseName::WAXING_GIBBOUS => 'J',
MoonPhaseName::FULL_MOON => '@',
MoonPhaseName::WANING_GIBBOUS => 'Q',
MoonPhaseName::THIRD_QUARTER => 'T',
MoonPhaseName::WANING_CRESCENT => 'W',
];
// Dutch description
[
MoonPhaseName::NEW_MOON => 'Nieuwe maan',
MoonPhaseName::WAXING_CRESCENT => '',
MoonPhaseName::FIRST_QUARTER => 'Eerste kwartier',
MoonPhaseName::WAXING_GIBBOUS => '',
MoonPhaseName::FULL_MOON => 'Volle maan',
MoonPhaseName::WANING_GIBBOUS => '',
MoonPhaseName::THIRD_QUARTER => 'Laatste kwartier',
MoonPhaseName::WANING_CRESCENT => '',
];
Implentatie (1)
De eenvoudigste manier om de maanfase aan de kalender toe te voegen, was
om in de week-loop, op elke dag een new MoonPhase($dt) object aan te
maken. Dan kon ik met ->getPhaseNameEnum() de fase van die datum
opvragen en aan de HTML toevoegen. Nadeel was dan wel, dat er bij
elke dag een symbool en omschrijving stond.
Implentatie (2)
OK, dan houden we bij welke fase als laatste is toegevoegd, en tonen ze alléén bij verandering. Dat werkte; de fases waren nog maar ééns per vier weken te zien. Maar ze kwamen qua datum niet overeen met die in de controletabel. Een fase kan blijkbaar tussen één en vier dagen duren. Bij één dag is de keuze duidelijk, bij drie kies je de middelste dag. Maar wat moest ik doen bij een fase van twee of vier dagen?
Implentatie (3)
Ik was niet tevreden en zocht in de eigenschappen van het MoonPhase()
object naar directe of indirecte aanwijzingen wanneer de verschillende
fasen optreden of op hun grootst zijn. Met name de methode ->getPhase()
gaf een interessante waarde: een float tussen 0.0 en 1.0, waarbij
(volgens de documentatie) zowel 0.0 als 1.0 staan voor Nieuwe maan
en 0.5 voor Volle maan.
Aldus ging ik creatief aan de slag om met ($phase % 0.25) == 0 op de
exact juiste dag de vier verschillende symbolen te kunnen neerzetten.
Tijdens deze stap ontdekte ik, dat PHP bij de modulo operator (%)
soms de waarden converteert naar integer - en in dit geval struikelde
over de waardes uit ->getPhase(). OK, dan gebruiken we fmod()...
Maar wat wil het geval? Mijn dag-loop rekent met 0:00 uur middernacht,
en de exacte tijdstippen van de maanfasen liggen daar zo goed als nooit.
Afronden tot op 1, 2, 3 of 4 cijfers mocht niet baten. Het werd één grote
spaghetti-puinhoop.
Implentatie (4)
Ik was nog steeds niet tevreden. Bij het lezen van de documentatie en
stoeien met de MoonPhase()-objecten, ontdekte ik de methode
getPhaseNewMoon(). Deze geeft van die betreffende fase de bijbehorende
(voorafgaande) Nieuwe maan als UNIX timestamp. Wat nou, als ik met een
sprong van 28 dagen het gehele jaar naloop en zo alle nieuwe manen
toevoeg? Dit bleek de juiste aanpak; ook voor de drie andere fasen waren
er passende ->getPhase...() methoden.
Finale: tijdzone
Tot zover was ik heel tevreden. Mijn datums kwamen mooi overeen met die
in de controletabel. Alle datums? Nee, niet alle datums. Eén kleine
datum (21 februari 2027) bleef moedig weerstand bieden aan de tot dan toe
bekende logica. Ik ging op zoek en zag het volgende bij een var_dump()
van het DateTime object:
object(DateTime)[397]
public 'date' => string '2027-02-20 23:24:11.473100' (length=26)
public 'timezone_type' => int 1
public 'timezone' => string '+00:00' (length=6)
Een timezone_type van 1 betekent dat er in timezone een verschil
in uren en minuten staat ten opzichte van UTC, de wereldwijde standaardtijd.
In dit geval dus 0 uren en 0 minuten om half twaalf 's nachts op 20 februari.
Maar ik wil graag mijn tijden zien met timezone_type = 3 en Europe/Amsterdam!
Aldus geschiedde en zie daar:
object(DateTime)[397]
public 'date' => string '2027-02-21 00:24:11.473100' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Amsterdam' (length=16)
De uiteindelijke code voor het toevoegen van de maanfases ziet er zo uit:
use \DateInterval;
use \DateTime;
use \DateTimeZone;
use \Solaris\MoonPhase;
use \Solaris\MoonPhaseName;
private function _addMoonEvents(int $year) : void
{
$tz = new DateTimeZone('Europe/Amsterdam');
$di28 = new DateInterval('P28D');
$dt = new DateTime($year . '-01-01', $tz);
$doIt = function (string $ymd, MoonPhaseName $e) {
$this->_addEvent(
$ymd,
sprintf(
'<span style="font-size:xx-small;">' .
'<span style="font-family:moonphases">%s</span> %s' .
'</span>',
CharFromMoonphase::g($e),
CharFromMoonphase::d($e)
)
);
};
while ($dt->format('Y') <= $year) {
$mp = new MoonPhase($dt);
$p = $mp->getPhaseNewMoon();
$dtP = DateTime::createFromFormat('U.u', $p)->setTimezone($tz);
$doIt($dtP->format('Y-m-d'), MoonPhaseName::NEW_MOON);
$p = $mp->getPhaseFirstQuarter();
$dtP = DateTime::createFromFormat('U.u', $p)->setTimezone($tz);
$doIt($dtP->format('Y-m-d'), MoonPhaseName::FIRST_QUARTER);
$p = $mp->getPhaseFullMoon();
$dtP = DateTime::createFromFormat('U.u', $p)->setTimezone($tz);
$doIt($dtP->format('Y-m-d'), MoonPhaseName::FULL_MOON);
$p = $mp->getPhaseLastQuarter();
$dtP = DateTime::createFromFormat('U.u', $p)->setTimezone($tz);
$doIt($dtP->format('Y-m-d'), MoonPhaseName::THIRD_QUARTER);
$dt = $dt->add($di28);
}
}