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