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