. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Functions; use Fisharebest\Webtrees\Age; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Date; use Fisharebest\Webtrees\Fact; use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\Gedcom; use Fisharebest\Webtrees\GedcomRecord; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Module\ModuleMapLinkInterface; use Fisharebest\Webtrees\Note; use Fisharebest\Webtrees\Place; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Services\GedcomEditService; use Fisharebest\Webtrees\Services\ModuleService; use Fisharebest\Webtrees\Tree; use Illuminate\Support\Str; use Ramsey\Uuid\Uuid; use function app; use function array_filter; use function e; use function explode; use function in_array; use function preg_match; use function preg_match_all; use function str_contains; use function strip_tags; use function strlen; use function strpos; use function substr; use function uasort; use function view; use const PREG_SET_ORDER; /** * Class FunctionsPrint - common functions * * @deprecated since 2.0.6. Will be removed in 2.1.0 */ class FunctionsPrint { /** * print a note record * * @param Tree $tree * @param string $text * @param int $nlevel the level of the note record * @param string $nrec the note record to print * * @return string */ private static function printNoteRecord(Tree $tree, string $text, int $nlevel, string $nrec): string { $element = Registry::elementFactory()->make('NOTE:CONC'); $text .= Functions::getCont($nlevel, $nrec); if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ NOTE/', $nrec, $match)) { // Shared note. $note = Registry::noteFactory()->make($match[1], $tree); // It must exist. assert($note instanceof Note); $label = I18N::translate('Shared note'); $html = $element->value($note->getNote(), $tree); $first_line = '' . $note->fullName() . ''; $one_line_only = strip_tags($note->fullName()) === strip_tags($note->getNote()); } else { // Inline note. $label = I18N::translate('Note'); $html = $element->value($text, $tree); [$first_line] = explode("\n", strip_tags($html)); // Use same logic as note objects $first_line = Str::limit($first_line, 100, I18N::translate('…')); $one_line_only = !str_contains($text, "\n") && mb_strlen($text) <= 100; } if ($one_line_only) { return '
' . I18N::translate( '%1$s: %2$s', $label, $html ) . '
'; } $id = 'collapse-' . Uuid::uuid4()->toString(); $expanded = (bool) $tree->getPreference('EXPAND_NOTES'); return '
' . '' . view('icons/expand') . view('icons/collapse') . '' . '' . $label . ': ' . $first_line . '
' . '
' . $html . '
'; } /** * Print all of the notes in this fact record * * @param Tree $tree * @param string $factrec The fact to print the notes from * @param int $level The level of the notes * * @return string HTML */ public static function printFactNotes(Tree $tree, string $factrec, int $level): string { $data = ''; $previous_spos = 0; $nlevel = $level + 1; $ct = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER); for ($j = 0; $j < $ct; $j++) { $spos1 = strpos($factrec, $match[$j][0], $previous_spos); $spos2 = strpos($factrec . "\n$level", "\n$level", $spos1 + 1); if (!$spos2) { $spos2 = strlen($factrec); } $previous_spos = $spos2; $nrec = substr($factrec, $spos1, $spos2 - $spos1); if (!isset($match[$j][1])) { $match[$j][1] = ''; } if (!preg_match('/^@(' . Gedcom::REGEX_XREF . ')@$/', $match[$j][1], $nmatch)) { $data .= self::printNoteRecord($tree, $match[$j][1], $nlevel, $nrec); } else { $note = Registry::noteFactory()->make($nmatch[1], $tree); if ($note) { if ($note->canShow()) { $noterec = $note->gedcom(); $nt = preg_match("/0 @$nmatch[1]@ NOTE (.*)/", $noterec, $n1match); $data .= self::printNoteRecord($tree, $nt > 0 ? $n1match[1] : '', 1, $noterec); } } else { $data = '
' . I18N::translate('Note') . ': ' . $nmatch[1] . '
'; } } } return $data; } /** * Format age of parents in HTML * * @param Individual $person child * @param Date $birth_date * * @return string HTML */ public static function formatParentsAges(Individual $person, Date $birth_date): string { $html = ''; $families = $person->childFamilies(); // Multiple sets of parents (e.g. adoption) cause complications, so ignore. if ($birth_date->isOK() && $families->count() === 1) { $family = $families->first(); foreach ($family->spouses() as $parent) { if ($parent->getBirthDate()->isOK()) { $sex = '' . view('icons/sex', ['sex' => $parent->sex()]) . ''; $age = new Age($parent->getBirthDate(), $birth_date); $deatdate = $parent->getDeathDate(); switch ($parent->sex()) { case 'F': // Highlight mothers who die in childbirth or shortly afterwards if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay() + 90) { $html .= ' ' . $sex . I18N::number($age->ageYears()) . ''; } else { $html .= ' ' . $sex . I18N::number($age->ageYears()) . ''; } break; case 'M': // Highlight fathers who die before the birth if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay()) { $html .= ' ' . $sex . I18N::number($age->ageYears()) . ''; } else { $html .= ' ' . $sex . I18N::number($age->ageYears()) . ''; } break; default: $html .= ' ' . $sex . I18N::number($age->ageYears()) . ''; break; } } } if ($html) { $html = '' . $html . ''; } } return $html; } /** * Print fact DATE/TIME * * @param Fact $event event containing the date/age * @param GedcomRecord $record the person (or couple) whose ages should be printed * @param bool $anchor option to print a link to calendar * @param bool $time option to print TIME value * * @return string */ public static function formatFactDate(Fact $event, GedcomRecord $record, bool $anchor, bool $time): string { $element_factory = Registry::elementFactory(); $factrec = $event->gedcom(); $html = ''; // Recorded age if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) { $fact_age = $element_factory->make($event->tag() . ':AGE')->value($match[1], $record->tree()); } else { $fact_age = ''; } if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) { $husb_age = $element_factory->make($event->tag() . ':HUSB:AGE')->value($match[1], $record->tree()); } else { $husb_age = ''; } if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) { $wife_age = $element_factory->make($event->tag() . ':WIFE:AGE')->value($match[1], $record->tree()); } else { $wife_age = ''; } // Calculated age [, $fact] = explode(':', $event->tag()); if (preg_match('/\n2 DATE (.+)/', $factrec, $match)) { $date = new Date($match[1]); $html .= ' ' . $date->display($anchor ? $record->tree() : null, null, $anchor); // time if ($time && preg_match('/\n3 TIME (.+)/', $factrec, $match)) { $html .= ' – ' . $match[1] . ''; } if ($record instanceof Individual) { if ( in_array($fact, Gedcom::BIRTH_EVENTS, true) && $record === $event->record() && $record->tree()->getPreference('SHOW_PARENTS_AGE') === '1' ) { // age of parents at child birth $html .= self::formatParentsAges($record, $date); } if ($fact !== 'BIRT' && $fact !== 'CHAN' && $fact !== '_TODO') { // age at event $birth_date = $record->getBirthDate(); // Can't use getDeathDate(), as this also gives BURI/CREM events, which // wouldn't give the correct "days after death" result for people with // no DEAT. $death_event = $record->facts(['DEAT'])->first(); if ($death_event instanceof Fact) { $death_date = $death_event->date(); } else { $death_date = new Date(''); } $ageText = ''; if ($fact === 'DEAT' || Date::compare($date, $death_date) <= 0 || !$record->isDead()) { // Before death, print age $age = (string) new Age($birth_date, $date); // Only show calculated age if it differs from recorded age if ($age !== '') { if ( $fact_age !== '' && !str_starts_with($fact_age, $age) || $fact_age === '' && $husb_age === '' && $wife_age === '' || $husb_age !== '' && !str_starts_with($husb_age, $age) && $record->sex() === 'M' || $wife_age !== '' && !str_starts_with($wife_age, $age) && $record->sex() === 'F' ) { switch ($record->sex()) { case 'M': /* I18N: The age of an individual at a given date */ $ageText = I18N::translateContext('Male', '(aged %s)', $age); break; case 'F': /* I18N: The age of an individual at a given date */ $ageText = I18N::translateContext('Female', '(aged %s)', $age); break; default: /* I18N: The age of an individual at a given date */ $ageText = I18N::translate('(aged %s)', $age); break; } } } } if ($fact !== 'DEAT' && $death_date->isOK() && Date::compare($death_date, $date) <= 0) { $death_day = $death_date->minimumDate()->day(); $event_day = $date->minimumDate()->day(); if ($death_day !== 0 && $event_day !== 0 && Date::compare($death_date, $date) === 0) { // On the exact date of death? // NOTE: this path is never reached. Keep the code (translation) in case // we decide to re-introduce it. $ageText = I18N::translate('(on the date of death)'); } else { // After death $age = (string) new Age($death_date, $date); $ageText = I18N::translate('(%s after death)', $age); } // Family events which occur after death are probably errors if ($event->record() instanceof Family) { $ageText .= view('icons/warning'); } } if ($ageText !== '') { $html .= ' ' . $ageText . ''; } } } } // print gedcom ages $age_labels = [ I18N::translate('Age') => $fact_age, I18N::translate('Husband') => $husb_age, I18N::translate('Wife') => $wife_age, ]; foreach (array_filter($age_labels) as $label => $age) { $html .= ' ' . $label . ': ' . $age . ''; } return $html; } /** * print fact PLACe TEMPle STATus * * @param Fact $event gedcom fact record * @param bool $anchor to print a link to placelist * @param bool $sub_records to print place subrecords * @param bool $lds to print LDS TEMPle and STATus * * @return string HTML */ public static function formatFactPlace(Fact $event, bool $anchor, bool $sub_records, bool $lds): string { $tree = $event->record()->tree(); if ($anchor) { // Show the full place name, for facts/events tab $html = $event->place()->fullName(true); } else { // Abbreviate the place name, for chart boxes return $event->place()->shortName(); } if ($sub_records) { $placerec = Functions::getSubRecord(2, '2 PLAC', $event->gedcom()); if ($placerec !== '') { if (preg_match_all('/\n3 (?:_HEB|ROMN) (.+)/', $placerec, $matches)) { foreach ($matches[1] as $match) { $wt_place = new Place($match, $tree); $html .= ' - ' . $wt_place->fullName(); } } $latitude = $event->latitude(); $longitude = $event->longitude(); if ($latitude !== null && $longitude !== null) { $html .= '
' . I18N::translate('Latitude') . ': ' . $latitude; $html .= ' ' . I18N::translate('Longitude') . ': ' . $longitude; // Links to external maps $html .= app(ModuleService::class) ->findByInterface(ModuleMapLinkInterface::class) ->map(fn (ModuleMapLinkInterface $module): string => ' ' . $module->mapLink($event)) ->implode(''); } if (preg_match('/\d NOTE (.*)/', $placerec, $match)) { $html .= '
' . self::printFactNotes($tree, $placerec, 3); } } } if ($lds) { if (preg_match('/2 TEMP (.*)/', $event->gedcom(), $match)) { $element = Registry::elementFactory()->make($event->tag() . ':TEMP'); $html .= $element->labelValue($match[1], $tree); } if (preg_match('/2 STAT (.*)/', $event->gedcom(), $match)) { $element = Registry::elementFactory()->make($event->tag() . ':STAT'); $html .= $element->labelValue($match[1], $tree); if (preg_match('/3 DATE (.*)/', $event->gedcom(), $match)) { $date = new Date($match[1]); $element = Registry::elementFactory()->make($event->tag() . ':STAT:DATE'); $html .= $element->labelValue($date->display(), $tree); } } } return $html; } /** * Print a new fact box on details pages * * @param Individual|Family $record * * @return void */ public static function printAddNewFact(GedcomRecord $record): void { $tree = $record->tree(); $add_facts = (new GedcomEditService())->factsToAdd($record, false); // Add from pick list switch ($record->tag()) { case Individual::RECORD_TYPE: $quick_facts = explode(',', $tree->getPreference('INDI_FACTS_QUICK')); $unique_facts = [ 'ADOP', 'AFN', 'BAPL', 'BAPM', 'BARM', 'BASM', 'BIRT', 'BURI', 'CAST', 'CHAN', 'CHR', 'CHRA', 'CONF', 'CONL', 'CREM', 'DEAT', 'ENDL', 'FCOM', 'GRAD', 'NCHI', 'NMR', 'ORDN', 'PROB', 'REFN', 'RELI', 'RESN', 'RETI', 'RFN', 'RIN', 'SEX', 'SLGC', 'SSN', 'WILL', ]; break; case Family::RECORD_TYPE: $quick_facts = explode(',', $tree->getPreference('FAM_FACTS_QUICK')); $unique_facts = [ 'DIV', 'DIVF', 'ENGA', 'MARR', ]; break; default: $quick_facts = []; $unique_facts = []; break; } // Filter existing tags $filter_fn = static fn (string $tag): bool => !in_array($tag, $unique_facts, true) || $record->facts([$tag])->isEmpty(); $quick_facts = array_filter($quick_facts, $filter_fn); // Create a label for a subtag $label_fn = static fn (string $subtag): string => Registry::elementFactory()->make($record->tag() . ':' . $subtag)->label(); $quick_facts = array_combine($quick_facts, array_map($label_fn, $quick_facts)); $add_facts = array_combine($add_facts, array_map($label_fn, $add_facts)); uasort($add_facts, I18N::comparator()); if (!Auth::canUploadMedia($record->tree(), Auth::user())) { unset($add_facts['OBJE'], $quick_facts['OBJE']); } echo view('edit/add-fact-row', [ 'add_facts' => $add_facts, 'quick_facts' => $quick_facts, 'record' => $record, ]); } }