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