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