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