. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Functions\FunctionsPrint; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Menu; use Fisharebest\Webtrees\Tree; use Fisharebest\Webtrees\User; use stdClass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Class FamilyBookChartModule */ class FamilyBookChartModule extends AbstractModule implements ModuleChartInterface { use ModuleChartTrait; // Defaults private const DEFAULT_GENERATIONS = '2'; private const DEFAULT_DESCENDANT_GENERATIONS = '5'; private const DEFAULT_MAXIMUM_GENERATIONS = '9'; /** @var stdClass */ private $box; /** @var bool */ private $show_spouse; /** @var int */ private $descent; /** @var int */ private $bhalfheight; /** @var int */ private $generations; /** @var int */ private $dgenerations; /** * How should this module be labelled on tabs, menus, etc.? * * @return string */ public function title(): string { /* I18N: Name of a module/chart */ return I18N::translate('Family book'); } /** * A sentence describing what this module does. * * @return string */ public function description(): string { /* I18N: Description of the “FamilyBookChart” module */ return I18N::translate('A chart of an individual’s ancestors and descendants, as a family book.'); } /** * CSS class for the URL. * * @return string */ public function chartMenuClass(): string { return 'menu-chart-familybook'; } /** * Return a menu item for this chart - for use in individual boxes. * * @param Individual $individual * * @return Menu|null */ public function chartBoxMenu(Individual $individual): ?Menu { return $this->chartMenu($individual); } /** * The title for a specific instance of this chart. * * @param Individual $individual * * @return string */ public function chartTitle(Individual $individual): string { /* I18N: %s is an individual’s name */ return I18N::translate('Family book of %s', $individual->getFullName()); } /** * A form to request the chart parameters. * * @param Request $request * @param Tree $tree * @param User $user * * @return Response */ public function getChartAction(Request $request, Tree $tree, User $user): Response { $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); Auth::checkIndividualAccess($individual); Auth::checkComponentAccess($this, 'chart', $tree, $user); $minimum_generations = 2; $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); $show_spouse = (bool) $request->get('show_spouse'); $generations = (int) $request->get('generations', $default_generations); $generations = min($generations, $maximum_generations); $generations = max($generations, $minimum_generations); // Generations of ancestors/descendants in each mini-tree. $book_size = (int) $request->get('book_size', 2); $book_size = min($book_size, 5); $book_size = max($book_size, 2); if ($ajax) { return $this->chart($individual, $generations, $book_size, $show_spouse); } $ajax_url = $this->chartUrl($individual, [ 'ajax' => true, 'book_size' => $book_size, 'generations' => $generations, 'show_spouse' => $show_spouse ]); return $this->viewResponse('modules/family-book-chart/page', [ 'ajax_url' => $ajax_url, 'book_size' => $book_size, 'generations' => $generations, 'individual' => $individual, 'maximum_generations' => $maximum_generations, 'minimum_generations' => $minimum_generations, 'module_name' => $this->name(), 'show_spouse' => $show_spouse, 'title' => $this->chartTitle($individual), ]); } /** * @param Individual $individual * @param int $generations * @param int $book_size * @param bool $show_spouse * * @return Response */ public function chart(Individual $individual, int $generations, int $book_size, bool $show_spouse): Response { $this->box = (object) [ 'width' => app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), 'height' => app()->make(ModuleThemeInterface::class)->parameter('chart-box-y'), ]; $this->show_spouse = $show_spouse; $this->descent = $generations; $this->generations = $book_size; $this->bhalfheight = $this->box->height / 2; $this->dgenerations = $this->maxDescendencyGenerations($individual, 0); if ($this->dgenerations < 1) { $this->dgenerations = 1; } // @TODO - this is just a wrapper around the old code. ob_start(); $this->printFamilyBook($individual, $generations); $html = ob_get_clean(); return new Response($html); } /** * Prints descendency of passed in person * * @param int $generation * @param Individual|null $person * * @return float */ private function printDescendency($generation, Individual $person = null): float { if ($generation > $this->dgenerations) { return 0; } echo ''; echo ''; echo '
'; $numkids = 0.0; // Load children $children = []; if ($person instanceof Individual) { // Count is position from center to left, dgenerations is number of generations if ($generation < $this->dgenerations) { // All children, from all partners foreach ($person->getSpouseFamilies() as $family) { foreach ($family->getChildren() as $child) { $children[] = $child; } } } } if ($generation < $this->dgenerations) { if (!empty($children)) { // real people echo ''; foreach ($children as $i => $child) { echo ''; // Print the lines if (count($children) > 1) { if ($i === 0) { // Adjust for the first column on left $h = round(((($this->box->height) * $kids) + 8) / 2); // Assumes border = 1 and padding = 3 // Adjust for other vertical columns if ($kids > 1) { $h = ($kids - 1) * 4 + $h; } echo ''; } elseif ($i === count($children) - 1) { // Adjust for the first column on left $h = round(((($this->box->height) * $kids) + 8) / 2); // Adjust for other vertical columns if ($kids > 1) { $h = ($kids - 1) * 4 + $h; } echo ''; } else { echo ''; } } echo ''; } echo '
'; $kids = $this->printDescendency($generation + 1, $child); $numkids += $kids; echo '', '', '', '
'; } else { // Hidden/empty boxes - to preserve the layout echo '
'; $numkids += $this->printDescendency($generation + 1, null); echo '
'; } echo '
'; } if ($numkids === 0.0) { $numkids = 1; } echo '
'; if ($person instanceof Individual) { echo FunctionsPrint::printPedigreePerson($person); echo '', ''; } else { echo '
', '
'; } // Print the spouse if ($generation === 1 && $person instanceof Individual) { if ($this->show_spouse) { foreach ($person->getSpouseFamilies() as $family) { $spouse = $family->getSpouse($person); echo '
'; echo FunctionsPrint::printPedigreePerson($spouse); $numkids += 0.95; echo ''; } } } echo '
'; echo '
'; return $numkids; } /** * Prints pedigree of the person passed in * * @param Individual $person * @param int $count * * @return void */ private function printPersonPedigree($person, $count) { if ($count >= $this->generations) { return; } $genoffset = $this->generations; // handle pedigree n generations lines //-- calculate how tall the lines should be $lh = ($this->bhalfheight) * (2 ** ($genoffset - $count - 1)); // //Prints empty table columns for children w/o parents up to the max generation //This allows vertical line spacing to be consistent if (count($person->getChildFamilies()) == 0) { echo ''; $this->printEmptyBox(); //-- recursively get the father’s family $this->printPersonPedigree($person, $count + 1); echo ''; $this->printEmptyBox(); //-- recursively get the mother’s family $this->printPersonPedigree($person, $count + 1); echo '
'; } // Empty box section done, now for regular pedigree foreach ($person->getChildFamilies() as $family) { echo '', '', ''; if ($family->getHusband()) { echo ''; } else { echo '', '', '', ''; if ($family->getWife()) { echo ''; } else { echo '', '
'; // Determine line height for two or more spouces // And then adjust the vertical line for the root person only $famcount = 0; if ($this->show_spouse) { // count number of spouses $famcount += count($person->getSpouseFamilies()); } $savlh = $lh; // Save current line height if ($count == 1 && $genoffset <= $famcount) { $linefactor = 0; // genoffset of 2 needs no adjustment if ($genoffset > 2) { $tblheight = $this->box->height + 8; if ($genoffset == 3) { if ($famcount == 3) { $linefactor = $tblheight / 2; } elseif ($famcount > 3) { $linefactor = $tblheight; } } if ($genoffset == 4) { if ($famcount == 4) { $linefactor = $tblheight; } elseif ($famcount > 4) { $linefactor = ($famcount - $genoffset) * ($tblheight * 1.5); } } if ($genoffset == 5) { if ($famcount == 5) { $linefactor = 0; } elseif ($famcount > 5) { $linefactor = $tblheight * ($famcount - $genoffset); } } } $lh = (($famcount - 1) * ($this->box->height) - ($linefactor)); if ($genoffset > 5) { $lh = $savlh; } } echo '', ''; $lh = $savlh; // restore original line height //-- print the father box echo FunctionsPrint::printPedigreePerson($family->getHusband()); echo ''; //-- recursively get the father’s family $this->printPersonPedigree($family->getHusband(), $count + 1); echo ''; if ($genoffset > $count) { echo ''; for ($i = 1; $i < (pow(2, ($genoffset) - $count) / 2); $i++) { $this->printEmptyBox(); echo ''; } echo '
'; } } echo '
'; //-- print the mother box echo FunctionsPrint::printPedigreePerson($family->getWife()); echo ''; //-- recursively print the mother’s family $this->printPersonPedigree($family->getWife(), $count + 1); echo ''; if ($count < $genoffset - 1) { echo ''; for ($i = 1; $i < (pow(2, ($genoffset - 1) - $count) / 2) + 1; $i++) { $this->printEmptyBox(); echo ''; $this->printEmptyBox(); echo ''; } echo '
'; } } echo '
'; break; } } /** * Calculates number of generations a person has * * @param Individual $individual * @param int $depth * * @return int */ private function maxDescendencyGenerations(Individual $individual, $depth): int { if ($depth > $this->generations) { return $depth; } $maxdc = $depth; foreach ($individual->getSpouseFamilies() as $family) { foreach ($family->getChildren() as $child) { $dc = $this->maxDescendencyGenerations($child, $depth + 1); if ($dc >= $this->generations) { return $dc; } if ($dc > $maxdc) { $maxdc = $dc; } } } $maxdc++; if ($maxdc == 1) { $maxdc++; } return $maxdc; } /** * Print empty box * * @return void */ private function printEmptyBox() { echo app()->make(ModuleThemeInterface::class)->individualBoxEmpty(); } /** * Print a “Family Book” for an individual * * @param Individual $person * @param int $descent_steps * * @return void */ private function printFamilyBook(Individual $person, $descent_steps) { if ($descent_steps == 0) { return; } echo '

', /* I18N: %s is an individual’s name */ I18N::translate('Family of %s', $person->getFullName()), '

', '
'; $this->dgenerations = $this->generations; $this->printDescendency(1, $person); echo ''; $this->printPersonPedigree($person, 1); echo '





'; foreach ($person->getSpouseFamilies() as $family) { foreach ($family->getChildren() as $child) { $this->printFamilyBook($child, $descent_steps - 1); } } } }