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\Family; 23use Fisharebest\Webtrees\FontAwesome; 24use Fisharebest\Webtrees\Functions\FunctionsCharts; 25use Fisharebest\Webtrees\Functions\FunctionsPrint; 26use Fisharebest\Webtrees\Gedcom; 27use Fisharebest\Webtrees\GedcomTag; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Individual; 30use Fisharebest\Webtrees\Menu; 31use Fisharebest\Webtrees\Services\ChartService; 32use Fisharebest\Webtrees\Tree; 33use Illuminate\Support\Collection; 34use Ramsey\Uuid\Uuid; 35use Symfony\Component\HttpFoundation\Request; 36use Symfony\Component\HttpFoundation\Response; 37 38/** 39 * Class DescendancyChartModule 40 */ 41class DescendancyChartModule extends AbstractModule implements ModuleChartInterface 42{ 43 use ModuleChartTrait; 44 45 // Chart styles 46 public const CHART_STYLE_LIST = 0; 47 public const CHART_STYLE_BOOKLET = 1; 48 public const CHART_STYLE_INDIVIDUALS = 2; 49 public const CHART_STYLE_FAMILIES = 3; 50 51 // Defaults 52 public const DEFAULT_STYLE = self::CHART_STYLE_LIST; 53 public const DEFAULT_GENERATIONS = '3'; 54 public const DEFAULT_MAXIMUM_GENERATIONS = '9'; 55 56 /** @var int[] */ 57 protected $dabo_num = []; 58 59 /** @var string[] */ 60 protected $dabo_sex = []; 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('Descendants'); 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 “DescendancyChart” module */ 81 return I18N::translate('A chart of an individual’s descendants.'); 82 } 83 84 /** 85 * CSS class for the URL. 86 * 87 * @return string 88 */ 89 public function chartMenuClass(): string 90 { 91 return 'menu-chart-descendants'; 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('Descendants 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 UserInterface $user 125 * @param ChartService $chart_service 126 * 127 * @return Response 128 */ 129 public function getChartAction(Request $request, Tree $tree, UserInterface $user, ChartService $chart_service): Response 130 { 131 $ajax = (bool) $request->get('ajax'); 132 $xref = $request->get('xref', ''); 133 $individual = Individual::getInstance($xref, $tree); 134 135 Auth::checkIndividualAccess($individual); 136 Auth::checkComponentAccess($this, 'chart', $tree, $user); 137 138 $minimum_generations = 2; 139 $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 140 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 141 142 $chart_style = (int) $request->get('chart_style', self::DEFAULT_STYLE); 143 $generations = (int) $request->get('generations', $default_generations); 144 145 $generations = min($generations, $maximum_generations); 146 $generations = max($generations, $minimum_generations); 147 148 if ($ajax) { 149 return $this->chart($request, $tree, $chart_service); 150 } 151 152 $ajax_url = $this->chartUrl($individual, [ 153 'chart_style' => $chart_style, 154 'generations' => $generations, 155 'ajax' => true, 156 ]); 157 158 return $this->viewResponse('modules/descendancy_chart/page', [ 159 'ajax_url' => $ajax_url, 160 'chart_style' => $chart_style, 161 'chart_styles' => $this->chartStyles(), 162 'default_generations' => $default_generations, 163 'generations' => $generations, 164 'individual' => $individual, 165 'maximum_generations' => $maximum_generations, 166 'minimum_generations' => $minimum_generations, 167 'module_name' => $this->name(), 168 'title' => $this->chartTitle($individual), 169 ]); 170 } 171 172 /** 173 * @param Request $request 174 * @param Tree $tree 175 * @param ChartService $chart_service 176 * 177 * @return Response 178 */ 179 public function chart(Request $request, Tree $tree, ChartService $chart_service): Response 180 { 181 $this->layout = 'layouts/ajax'; 182 183 $xref = $request->get('xref', ''); 184 $individual = Individual::getInstance($xref, $tree); 185 186 Auth::checkIndividualAccess($individual); 187 188 $minimum_generations = 2; 189 $maximum_generations = (int) $tree->getPreference('MAX_PEDIGREE_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 190 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 191 192 $chart_style = (int) $request->get('chart_style', self::DEFAULT_STYLE); 193 $generations = (int) $request->get('generations', $default_generations); 194 195 $generations = min($generations, $maximum_generations); 196 $generations = max($generations, $minimum_generations); 197 198 switch ($chart_style) { 199 case self::CHART_STYLE_LIST: 200 default: 201 return $this->descendantsList($individual, $generations); 202 203 case self::CHART_STYLE_BOOKLET: 204 return $this->descendantsBooklet($individual, $generations); 205 206 case self::CHART_STYLE_INDIVIDUALS: 207 $individuals = $chart_service->descendants($individual, $generations - 1); 208 209 return $this->descendantsIndividuals($tree, $individuals); 210 211 case self::CHART_STYLE_FAMILIES: 212 $families = $chart_service->descendantFamilies($individual, $generations - 1); 213 214 return $this->descendantsFamilies($tree, $families); 215 } 216 } 217 218 /** 219 * Show a hierarchical list of descendants 220 * 221 * @TODO replace ob_start() with views. 222 * 223 * @param Individual $individual 224 * @param int $generations 225 * 226 * @return Response 227 */ 228 private function descendantsList(Individual $individual, int $generations): Response 229 { 230 ob_start(); 231 232 echo '<ul class="chart_common">'; 233 $this->printChildDescendancy($individual, $generations, $generations); 234 echo '</ul>'; 235 236 $html = ob_get_clean(); 237 238 return new Response($html); 239 } 240 241 /** 242 * print a child descendancy 243 * 244 * @param Individual $person 245 * @param int $depth the descendancy depth to show 246 * @param int $generations 247 * 248 * @return void 249 */ 250 private function printChildDescendancy(Individual $person, $depth, int $generations) 251 { 252 echo '<li>'; 253 echo '<table><tr><td>'; 254 if ($depth == $generations) { 255 echo '<img alt="" role="presentation" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-spacer') . '" height="3" width="15"></td><td>'; 256 } else { 257 echo '<img src="' . app()->make(ModuleThemeInterface::class)->parameter('image-spacer') . '" height="3" width="3">'; 258 echo '<img src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" height="3" width="', 12, '"></td><td>'; 259 } 260 echo FunctionsPrint::printPedigreePerson($person); 261 echo '</td>'; 262 263 // check if child has parents and add an arrow 264 echo '<td></td>'; 265 echo '<td>'; 266 foreach ($person->getChildFamilies() as $cfamily) { 267 foreach ($cfamily->getSpouses() as $parent) { 268 echo FontAwesome::linkIcon('arrow-up', I18N::translate('Start at parents'), [ 269 'href' => route('descendants', [ 270 'ged' => $parent->tree()->name(), 271 'xref' => $parent->xref(), 272 'generations' => $generations, 273 ]), 274 ]); 275 // only show the arrow for one of the parents 276 break; 277 } 278 } 279 280 // d'Aboville child number 281 $level = $generations - $depth; 282 echo '<br><br> '; 283 echo '<span dir="ltr">'; //needed so that RTL languages will display this properly 284 if (!isset($this->dabo_num[$level])) { 285 $this->dabo_num[$level] = 0; 286 } 287 $this->dabo_num[$level]++; 288 $this->dabo_num[$level + 1] = 0; 289 $this->dabo_sex[$level] = $person->getSex(); 290 for ($i = 0; $i <= $level; $i++) { 291 $isf = $this->dabo_sex[$i]; 292 if ($isf === 'M') { 293 $isf = ''; 294 } 295 if ($isf === 'U') { 296 $isf = 'NN'; 297 } 298 echo '<span class="person_box' . $isf . '"> ' . $this->dabo_num[$i] . ' </span>'; 299 if ($i < $level) { 300 echo '.'; 301 } 302 } 303 echo '</span>'; 304 echo '</td></tr>'; 305 echo '</table>'; 306 echo '</li>'; 307 308 // loop for each spouse 309 foreach ($person->getSpouseFamilies() as $family) { 310 $this->printFamilyDescendancy($person, $family, $depth, $generations); 311 } 312 } 313 314 /** 315 * print a family descendancy 316 * 317 * @param Individual $person 318 * @param Family $family 319 * @param int $depth the descendancy depth to show 320 * @param int $generations 321 * 322 * @return void 323 */ 324 private function printFamilyDescendancy(Individual $person, Family $family, int $depth, int $generations) 325 { 326 $uid = Uuid::uuid4()->toString(); // create a unique ID 327 // print marriage info 328 echo '<li>'; 329 echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="2" width="', 19, '">'; 330 echo '<span class="details1">'; 331 echo '<a href="#" onclick="expand_layer(\'' . $uid . '\'); return false;" class="top"><i id="' . $uid . '_img" class="icon-minus" title="' . I18N::translate('View this family') . '"></i></a>'; 332 if ($family->canShow()) { 333 foreach ($family->facts(Gedcom::MARRIAGE_EVENTS) as $fact) { 334 echo ' <a href="', e($family->url()), '" class="details1">', $fact->summary(), '</a>'; 335 } 336 } 337 echo '</span>'; 338 339 // print spouse 340 $spouse = $family->getSpouse($person); 341 echo '<ul class="generations" id="' . $uid . '">'; 342 echo '<li>'; 343 echo '<table><tr><td>'; 344 echo FunctionsPrint::printPedigreePerson($spouse); 345 echo '</td>'; 346 347 // check if spouse has parents and add an arrow 348 echo '<td></td>'; 349 echo '<td>'; 350 if ($spouse) { 351 foreach ($spouse->getChildFamilies() as $cfamily) { 352 foreach ($cfamily->getSpouses() as $parent) { 353 echo FontAwesome::linkIcon('arrow-up', I18N::translate('Start at parents'), [ 354 'href' => route('descendants', [ 355 'ged' => $parent->tree()->name(), 356 'xref' => $parent->xref(), 357 'generations' => $generations, 358 ]), 359 ]); 360 // only show the arrow for one of the parents 361 break; 362 } 363 } 364 } 365 echo '<br><br> '; 366 echo '</td></tr>'; 367 368 // children 369 $children = $family->getChildren(); 370 echo '<tr><td colspan="3" class="details1" > '; 371 if (!empty($children)) { 372 echo GedcomTag::getLabel('NCHI') . ': ' . count($children); 373 } else { 374 // Distinguish between no children (NCHI 0) and no recorded 375 // children (no CHIL records) 376 if (strpos($family->gedcom(), '\n1 NCHI 0') !== false) { 377 echo GedcomTag::getLabel('NCHI') . ': ' . count($children); 378 } else { 379 echo I18N::translate('No children'); 380 } 381 } 382 echo '</td></tr></table>'; 383 echo '</li>'; 384 if ($depth > 1) { 385 foreach ($children as $child) { 386 $this->printChildDescendancy($child, $depth - 1, $generations); 387 } 388 } 389 echo '</ul>'; 390 echo '</li>'; 391 } 392 393 /** 394 * Show a tabular list of individual descendants. 395 * 396 * @param Tree $tree 397 * @param Collection $individuals 398 * 399 * @return Response 400 */ 401 private function descendantsIndividuals(Tree $tree, Collection $individuals): Response 402 { 403 $this->layout = 'layouts/ajax'; 404 405 return $this->viewResponse('lists/individuals-table', [ 406 'individuals' => $individuals, 407 'sosa' => false, 408 'tree' => $tree, 409 ]); 410 } 411 412 /** 413 * Show a tabular list of individual descendants. 414 * 415 * @param Tree $tree 416 * @param Collection $families 417 * 418 * @return Response 419 */ 420 private function descendantsFamilies(Tree $tree, Collection $families): Response 421 { 422 $this->layout = 'layouts/ajax'; 423 424 return $this->viewResponse('lists/families-table', [ 425 'families' => $families, 426 'tree' => $tree, 427 ]); 428 } 429 430 /** 431 * Show a booklet view of descendants 432 * 433 * @TODO replace ob_start() with views. 434 * 435 * @param Individual $individual 436 * @param int $generations 437 * 438 * @return Response 439 */ 440 private function descendantsBooklet(Individual $individual, int $generations): Response 441 { 442 ob_start(); 443 444 $this->printChildFamily($individual, $generations); 445 446 $html = ob_get_clean(); 447 448 return new Response($html); 449 } 450 451 /** 452 * Print a child family 453 * 454 * @param Individual $individual 455 * @param int $depth - the descendancy depth to show 456 * @param string $daboville - d'Aboville number 457 * @param string $gpid 458 * 459 * @return void 460 */ 461 private function printChildFamily(Individual $individual, $depth, $daboville = '1.', $gpid = '') 462 { 463 if ($depth < 2) { 464 return; 465 } 466 467 $i = 1; 468 469 foreach ($individual->getSpouseFamilies() as $family) { 470 FunctionsCharts::printSosaFamily($family, '', -1, $daboville, $individual->xref(), $gpid, false); 471 foreach ($family->getChildren() as $child) { 472 $this->printChildFamily($child, $depth - 1, $daboville . ($i++) . '.', $individual->xref()); 473 } 474 } 475 } 476 477 /** 478 * This chart can display its output in a number of styles 479 * 480 * @return array 481 */ 482 private function chartStyles(): array 483 { 484 return [ 485 self::CHART_STYLE_LIST => I18N::translate('List'), 486 self::CHART_STYLE_BOOKLET => I18N::translate('Booklet'), 487 self::CHART_STYLE_INDIVIDUALS => I18N::translate('Individuals'), 488 self::CHART_STYLE_FAMILIES => I18N::translate('Families'), 489 ]; 490 } 491} 492