1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Module; 19 20use Fisharebest\Webtrees\Auth; 21use Fisharebest\Webtrees\Functions\FunctionsPrint; 22use Fisharebest\Webtrees\I18N; 23use Fisharebest\Webtrees\Individual; 24use Fisharebest\Webtrees\Menu; 25use Fisharebest\Webtrees\Theme; 26use Fisharebest\Webtrees\Tree; 27use Fisharebest\Webtrees\User; 28use stdClass; 29use Symfony\Component\HttpFoundation\Request; 30use Symfony\Component\HttpFoundation\Response; 31 32/** 33 * Class FamilyBookChartModule 34 */ 35class FamilyBookChartModule extends AbstractModule implements ModuleChartInterface 36{ 37 use ModuleChartTrait; 38 39 // Defaults 40 private const DEFAULT_GENERATIONS = '2'; 41 private const DEFAULT_DESCENDANT_GENERATIONS = '5'; 42 private const DEFAULT_MAXIMUM_GENERATIONS = '9'; 43 44 /** @var stdClass */ 45 private $box; 46 47 /** @var bool */ 48 private $show_spouse; 49 50 /** @var int */ 51 private $descent; 52 53 /** @var int */ 54 private $bhalfheight; 55 56 /** @var int */ 57 private $generations; 58 59 /** @var int */ 60 private $dgenerations; 61 62 /** 63 * How should this module be labelled on tabs, menus, etc.? 64 * 65 * @return string 66 */ 67 public function title(): string 68 { 69 /* I18N: Name of a module/chart */ 70 return I18N::translate('Family book'); 71 } 72 73 /** 74 * A sentence describing what this module does. 75 * 76 * @return string 77 */ 78 public function description(): string 79 { 80 /* I18N: Description of the “FamilyBookChart” module */ 81 return I18N::translate('A chart of an individual’s ancestors and descendants, as a family book.'); 82 } 83 84 /** 85 * CSS class for the URL. 86 * 87 * @return string 88 */ 89 public function chartMenuClass(): string 90 { 91 return 'menu-chart-familybook'; 92 } 93 94 /** 95 * Return a menu item for this chart - for use in individual boxes. 96 * 97 * @param Individual $individual 98 * 99 * @return Menu|null 100 */ 101 public function chartBoxMenu(Individual $individual): ?Menu 102 { 103 return $this->chartMenu($individual); 104 } 105 106 /** 107 * The title for a specific instance of this chart. 108 * 109 * @param Individual $individual 110 * 111 * @return string 112 */ 113 public function chartTitle(Individual $individual): string 114 { 115 /* I18N: %s is an individual’s name */ 116 return I18N::translate('Family book of %s', $individual->getFullName()); 117 } 118 119 /** 120 * A form to request the chart parameters. 121 * 122 * @param Request $request 123 * @param Tree $tree 124 * @param User $user 125 * 126 * @return Response 127 */ 128 public function getChartAction(Request $request, Tree $tree, User $user): Response 129 { 130 $ajax = (bool) $request->get('ajax'); 131 $xref = $request->get('xref', ''); 132 $individual = Individual::getInstance($xref, $tree); 133 134 Auth::checkIndividualAccess($individual); 135 Auth::checkComponentAccess($this, 'chart', $tree, $user); 136 137 $minimum_generations = 2; 138 $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 139 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 140 141 $show_spouse = (bool) $request->get('show_spouse'); 142 $generations = (int) $request->get('generations', $default_generations); 143 $generations = min($generations, $maximum_generations); 144 $generations = max($generations, $minimum_generations); 145 146 // Generations of ancestors/descendants in each mini-tree. 147 $book_size = (int) $request->get('book_size', 2); 148 $book_size = min($book_size, 5); 149 $book_size = max($book_size, 2); 150 151 if ($ajax) { 152 return $this->chart($individual, $generations, $book_size, $show_spouse); 153 } 154 155 $ajax_url = $this->chartUrl($individual, [ 156 'ajax' => true, 157 'book_size' => $book_size, 158 'generations' => $generations, 159 'show_spouse' => $show_spouse 160 ]); 161 162 return $this->viewResponse('modules/family-book-chart/page', [ 163 'ajax_url' => $ajax_url, 164 'book_size' => $book_size, 165 'generations' => $generations, 166 'individual' => $individual, 167 'maximum_generations' => $maximum_generations, 168 'minimum_generations' => $minimum_generations, 169 'module_name' => $this->name(), 170 'show_spouse' => $show_spouse, 171 'title' => $this->chartTitle($individual), 172 ]); 173 } 174 175 /** 176 * @param Individual $individual 177 * @param int $generations 178 * @param int $book_size 179 * @param bool $show_spouse 180 * 181 * @return Response 182 */ 183 public function chart(Individual $individual, int $generations, int $book_size, bool $show_spouse): Response 184 { 185 $this->box = (object) [ 186 'width' => Theme::theme()->parameter('chart-box-x'), 187 'height' => Theme::theme()->parameter('chart-box-y'), 188 ]; 189 190 $this->show_spouse = $show_spouse; 191 $this->descent = $generations; 192 $this->generations = $book_size; 193 194 $this->bhalfheight = $this->box->height / 2; 195 $this->dgenerations = $this->maxDescendencyGenerations($individual, 0); 196 197 if ($this->dgenerations < 1) { 198 $this->dgenerations = 1; 199 } 200 201 // @TODO - this is just a wrapper around the old code. 202 ob_start(); 203 $this->printFamilyBook($individual, $generations); 204 $html = ob_get_clean(); 205 206 return new Response($html); 207 } 208 209 /** 210 * Prints descendency of passed in person 211 * 212 * @param int $generation 213 * @param Individual|null $person 214 * 215 * @return float 216 */ 217 private function printDescendency($generation, Individual $person = null): float 218 { 219 if ($generation > $this->dgenerations) { 220 return 0; 221 } 222 223 echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>'; 224 $numkids = 0.0; 225 226 // Load children 227 $children = []; 228 if ($person instanceof Individual) { 229 // Count is position from center to left, dgenerations is number of generations 230 if ($generation < $this->dgenerations) { 231 // All children, from all partners 232 foreach ($person->getSpouseFamilies() as $family) { 233 foreach ($family->getChildren() as $child) { 234 $children[] = $child; 235 } 236 } 237 } 238 } 239 if ($generation < $this->dgenerations) { 240 if (!empty($children)) { 241 // real people 242 echo '<table cellspacing="0" cellpadding="0" border="0" >'; 243 foreach ($children as $i => $child) { 244 echo '<tr><td>'; 245 $kids = $this->printDescendency($generation + 1, $child); 246 $numkids += $kids; 247 echo '</td>'; 248 // Print the lines 249 if (count($children) > 1) { 250 if ($i === 0) { 251 // Adjust for the first column on left 252 $h = round(((($this->box->height) * $kids) + 8) / 2); // Assumes border = 1 and padding = 3 253 // Adjust for other vertical columns 254 if ($kids > 1) { 255 $h = ($kids - 1) * 4 + $h; 256 } 257 echo '<td class="align-bottom">', 258 '<img id="vline_', $child->xref(), '" src="', Theme::theme()->parameter('image-vline'), '" width="3" height="', $h - 4, '"></td>'; 259 } elseif ($i === count($children) - 1) { 260 // Adjust for the first column on left 261 $h = round(((($this->box->height) * $kids) + 8) / 2); 262 // Adjust for other vertical columns 263 if ($kids > 1) { 264 $h = ($kids - 1) * 4 + $h; 265 } 266 echo '<td class="align-top">', 267 '<img class="bvertline" width="3" id="vline_', $child->xref(), '" src="', Theme::theme()->parameter('image-vline'), '" height="', $h - 2, '"></td>'; 268 } else { 269 echo '<td class="align-bottomm"style="background: url(', Theme::theme()->parameter('image-vline'), ');">', 270 '<img class="spacer" width="3" src="', Theme::theme()->parameter('image-spacer'), '"></td>'; 271 } 272 } 273 echo '</tr>'; 274 } 275 echo '</table>'; 276 } else { 277 // Hidden/empty boxes - to preserve the layout 278 echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>'; 279 $numkids += $this->printDescendency($generation + 1, null); 280 echo '</td></tr></table>'; 281 } 282 echo '</td>'; 283 echo '<td>'; 284 } 285 286 if ($numkids === 0.0) { 287 $numkids = 1; 288 } 289 echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>'; 290 if ($person instanceof Individual) { 291 echo FunctionsPrint::printPedigreePerson($person); 292 echo '</td><td>', 293 '<img class="linef1" src="', Theme::theme()->parameter('image-hline'), '" width="8" height="3">'; 294 } else { 295 echo '<div style="width:', $this->box->width + 19, 'px; height:', $this->box->height + 8, 'px;"></div>', 296 '</td><td>'; 297 } 298 299 // Print the spouse 300 if ($generation === 1 && $person instanceof Individual) { 301 if ($this->show_spouse) { 302 foreach ($person->getSpouseFamilies() as $family) { 303 $spouse = $family->getSpouse($person); 304 echo '</td></tr><tr><td>'; 305 echo FunctionsPrint::printPedigreePerson($spouse); 306 $numkids += 0.95; 307 echo '</td><td>'; 308 } 309 } 310 } 311 echo '</td></tr></table>'; 312 echo '</td></tr>'; 313 echo '</table>'; 314 315 return $numkids; 316 } 317 318 /** 319 * Prints pedigree of the person passed in 320 * 321 * @param Individual $person 322 * @param int $count 323 * 324 * @return void 325 */ 326 private function printPersonPedigree($person, $count) 327 { 328 if ($count >= $this->generations) { 329 return; 330 } 331 332 $genoffset = $this->generations; // handle pedigree n generations lines 333 //-- calculate how tall the lines should be 334 $lh = ($this->bhalfheight) * (2 ** ($genoffset - $count - 1)); 335 // 336 //Prints empty table columns for children w/o parents up to the max generation 337 //This allows vertical line spacing to be consistent 338 if (count($person->getChildFamilies()) == 0) { 339 echo '<table cellspacing="0" cellpadding="0" border="0" >'; 340 $this->printEmptyBox(); 341 342 //-- recursively get the father’s family 343 $this->printPersonPedigree($person, $count + 1); 344 echo '</td><td></tr>'; 345 $this->printEmptyBox(); 346 347 //-- recursively get the mother’s family 348 $this->printPersonPedigree($person, $count + 1); 349 echo '</td><td></tr></table>'; 350 } 351 352 // Empty box section done, now for regular pedigree 353 foreach ($person->getChildFamilies() as $family) { 354 echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td class="align-bottom">'; 355 // Determine line height for two or more spouces 356 // And then adjust the vertical line for the root person only 357 $famcount = 0; 358 if ($this->show_spouse) { 359 // count number of spouses 360 $famcount += count($person->getSpouseFamilies()); 361 } 362 $savlh = $lh; // Save current line height 363 if ($count == 1 && $genoffset <= $famcount) { 364 $linefactor = 0; 365 // genoffset of 2 needs no adjustment 366 if ($genoffset > 2) { 367 $tblheight = $this->box->height + 8; 368 if ($genoffset == 3) { 369 if ($famcount == 3) { 370 $linefactor = $tblheight / 2; 371 } elseif ($famcount > 3) { 372 $linefactor = $tblheight; 373 } 374 } 375 if ($genoffset == 4) { 376 if ($famcount == 4) { 377 $linefactor = $tblheight; 378 } elseif ($famcount > 4) { 379 $linefactor = ($famcount - $genoffset) * ($tblheight * 1.5); 380 } 381 } 382 if ($genoffset == 5) { 383 if ($famcount == 5) { 384 $linefactor = 0; 385 } elseif ($famcount > 5) { 386 $linefactor = $tblheight * ($famcount - $genoffset); 387 } 388 } 389 } 390 $lh = (($famcount - 1) * ($this->box->height) - ($linefactor)); 391 if ($genoffset > 5) { 392 $lh = $savlh; 393 } 394 } 395 echo '<img class="line3 pvline" src="', Theme::theme()->parameter('image-vline'), '" width="3" height="', $lh, '"></td>', 396 '<td>', 397 '<img class="linef2" src="', Theme::theme()->parameter('image-hline'), '" height="3"></td>', 398 '<td>'; 399 $lh = $savlh; // restore original line height 400 //-- print the father box 401 echo FunctionsPrint::printPedigreePerson($family->getHusband()); 402 echo '</td>'; 403 if ($family->getHusband()) { 404 echo '<td>'; 405 //-- recursively get the father’s family 406 $this->printPersonPedigree($family->getHusband(), $count + 1); 407 echo '</td>'; 408 } else { 409 echo '<td>'; 410 if ($genoffset > $count) { 411 echo '<table cellspacing="0" cellpadding="0" border="0" >'; 412 for ($i = 1; $i < (pow(2, ($genoffset) - $count) / 2); $i++) { 413 $this->printEmptyBox(); 414 echo '</tr>'; 415 } 416 echo '</table>'; 417 } 418 } 419 echo '</tr><tr>', 420 '<td class="align-top"><img class="pvline" alt="" role="presentation" src="', Theme::theme()->parameter('image-vline'), '" width="3" height="', $lh, '"></td>', 421 '<td><img class="linef3" alt="" role="presentation" src="', Theme::theme()->parameter('image-hline'), '" height="3"></td>', 422 '<td>'; 423 //-- print the mother box 424 echo FunctionsPrint::printPedigreePerson($family->getWife()); 425 echo '</td>'; 426 if ($family->getWife()) { 427 echo '<td>'; 428 //-- recursively print the mother’s family 429 $this->printPersonPedigree($family->getWife(), $count + 1); 430 echo '</td>'; 431 } else { 432 echo '<td>'; 433 if ($count < $genoffset - 1) { 434 echo '<table cellspacing="0" cellpadding="0" border="0" >'; 435 for ($i = 1; $i < (pow(2, ($genoffset - 1) - $count) / 2) + 1; $i++) { 436 $this->printEmptyBox(); 437 echo '</tr>'; 438 $this->printEmptyBox(); 439 echo '</tr>'; 440 } 441 echo '</table>'; 442 } 443 } 444 echo '</tr>', 445 '</table>'; 446 break; 447 } 448 } 449 450 /** 451 * Calculates number of generations a person has 452 * 453 * @param Individual $individual 454 * @param int $depth 455 * 456 * @return int 457 */ 458 private function maxDescendencyGenerations(Individual $individual, $depth): int 459 { 460 if ($depth > $this->generations) { 461 return $depth; 462 } 463 $maxdc = $depth; 464 foreach ($individual->getSpouseFamilies() as $family) { 465 foreach ($family->getChildren() as $child) { 466 $dc = $this->maxDescendencyGenerations($child, $depth + 1); 467 if ($dc >= $this->generations) { 468 return $dc; 469 } 470 if ($dc > $maxdc) { 471 $maxdc = $dc; 472 } 473 } 474 } 475 $maxdc++; 476 if ($maxdc == 1) { 477 $maxdc++; 478 } 479 480 return $maxdc; 481 } 482 483 /** 484 * Print empty box 485 * 486 * @return void 487 */ 488 489 private function printEmptyBox() 490 { 491 echo Theme::theme()->individualBoxEmpty(); 492 } 493 494 /** 495 * Print a “Family Book” for an individual 496 * 497 * @param Individual $person 498 * @param int $descent_steps 499 * 500 * @return void 501 */ 502 private function printFamilyBook(Individual $person, $descent_steps) 503 { 504 if ($descent_steps == 0) { 505 return; 506 } 507 508 echo 509 '<h3>', 510 /* I18N: %s is an individual’s name */ 511 I18N::translate('Family of %s', $person->getFullName()), 512 '</h3>', 513 '<table cellspacing="0" cellpadding="0" border="0" ><tr><td class="align-middle">'; 514 $this->dgenerations = $this->generations; 515 $this->printDescendency(1, $person); 516 echo '</td><td class="align-middle">'; 517 $this->printPersonPedigree($person, 1); 518 echo '</td></tr></table><br><br><hr class="family-break"><br><br>'; 519 foreach ($person->getSpouseFamilies() as $family) { 520 foreach ($family->getChildren() as $child) { 521 $this->printFamilyBook($child, $descent_steps - 1); 522 } 523 } 524 } 525} 526