1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2020 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Aura\Router\RouterContainer; 23use Closure; 24use Fig\Http\Message\RequestMethodInterface; 25use Fisharebest\Algorithm\Dijkstra; 26use Fisharebest\Webtrees\Auth; 27use Fisharebest\Webtrees\Factory; 28use Fisharebest\Webtrees\FlashMessages; 29use Fisharebest\Webtrees\Functions\Functions; 30use Fisharebest\Webtrees\I18N; 31use Fisharebest\Webtrees\Individual; 32use Fisharebest\Webtrees\Menu; 33use Fisharebest\Webtrees\Services\TreeService; 34use Fisharebest\Webtrees\Tree; 35use Fisharebest\Webtrees\User; 36use Illuminate\Database\Capsule\Manager as DB; 37use Illuminate\Database\Query\JoinClause; 38use Psr\Http\Message\ResponseInterface; 39use Psr\Http\Message\ServerRequestInterface; 40use Psr\Http\Server\RequestHandlerInterface; 41 42use function app; 43use function assert; 44use function is_string; 45use function redirect; 46use function route; 47use function view; 48 49/** 50 * Class RelationshipsChartModule 51 */ 52class RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, ModuleConfigInterface, RequestHandlerInterface 53{ 54 use ModuleChartTrait; 55 use ModuleConfigTrait; 56 57 protected const ROUTE_URL = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}'; 58 59 /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */ 60 public const UNLIMITED_RECURSION = 99; 61 62 /** By default new trees allow unlimited recursion */ 63 public const DEFAULT_RECURSION = '99'; 64 65 /** By default new trees search for all relationships (not via ancestors) */ 66 public const DEFAULT_ANCESTORS = '0'; 67 public const DEFAULT_PARAMETERS = [ 68 'ancestors' => self::DEFAULT_ANCESTORS, 69 'recursion' => self::DEFAULT_RECURSION, 70 ]; 71 72 /** @var TreeService */ 73 private $tree_service; 74 75 /** 76 * ModuleController constructor. 77 * 78 * @param TreeService $tree_service 79 */ 80 public function __construct(TreeService $tree_service) 81 { 82 $this->tree_service = $tree_service; 83 } 84 85 /** 86 * Initialization. 87 * 88 * @return void 89 */ 90 public function boot(): void 91 { 92 $router_container = app(RouterContainer::class); 93 assert($router_container instanceof RouterContainer); 94 95 $router_container->getMap() 96 ->get(static::class, static::ROUTE_URL, $this) 97 ->allows(RequestMethodInterface::METHOD_POST) 98 ->tokens([ 99 'ancestors' => '\d+', 100 'recursion' => '\d+', 101 ]); 102 } 103 104 /** 105 * A sentence describing what this module does. 106 * 107 * @return string 108 */ 109 public function description(): string 110 { 111 /* I18N: Description of the “RelationshipsChart” module */ 112 return I18N::translate('A chart displaying relationships between two individuals.'); 113 } 114 115 /** 116 * Return a menu item for this chart - for use in individual boxes. 117 * 118 * @param Individual $individual 119 * 120 * @return Menu|null 121 */ 122 public function chartBoxMenu(Individual $individual): ?Menu 123 { 124 return $this->chartMenu($individual); 125 } 126 127 /** 128 * A main menu item for this chart. 129 * 130 * @param Individual $individual 131 * 132 * @return Menu 133 */ 134 public function chartMenu(Individual $individual): Menu 135 { 136 $gedcomid = $individual->tree()->getUserPreference(Auth::user(), User::PREF_TREE_ACCOUNT_XREF); 137 138 if ($gedcomid !== '' && $gedcomid !== $individual->xref()) { 139 return new Menu( 140 I18N::translate('Relationship to me'), 141 $this->chartUrl($individual, ['xref2' => $gedcomid]), 142 $this->chartMenuClass(), 143 $this->chartUrlAttributes() 144 ); 145 } 146 147 return new Menu( 148 $this->title(), 149 $this->chartUrl($individual), 150 $this->chartMenuClass(), 151 $this->chartUrlAttributes() 152 ); 153 } 154 155 /** 156 * CSS class for the URL. 157 * 158 * @return string 159 */ 160 public function chartMenuClass(): string 161 { 162 return 'menu-chart-relationship'; 163 } 164 165 /** 166 * How should this module be identified in the control panel, etc.? 167 * 168 * @return string 169 */ 170 public function title(): string 171 { 172 /* I18N: Name of a module/chart */ 173 return I18N::translate('Relationships'); 174 } 175 176 /** 177 * The URL for a page showing chart options. 178 * 179 * @param Individual $individual 180 * @param mixed[] $parameters 181 * 182 * @return string 183 */ 184 public function chartUrl(Individual $individual, array $parameters = []): string 185 { 186 return route(static::class, [ 187 'xref' => $individual->xref(), 188 'tree' => $individual->tree()->name(), 189 ] + $parameters + self::DEFAULT_PARAMETERS); 190 } 191 192 /** 193 * @param ServerRequestInterface $request 194 * 195 * @return ResponseInterface 196 */ 197 public function handle(ServerRequestInterface $request): ResponseInterface 198 { 199 $tree = $request->getAttribute('tree'); 200 assert($tree instanceof Tree); 201 202 $xref = $request->getAttribute('xref'); 203 assert(is_string($xref)); 204 205 $xref2 = $request->getAttribute('xref2') ?? ''; 206 207 $ajax = $request->getQueryParams()['ajax'] ?? ''; 208 $ancestors = (int) $request->getAttribute('ancestors'); 209 $recursion = (int) $request->getAttribute('recursion'); 210 $user = $request->getAttribute('user'); 211 212 // Convert POST requests into GET requests for pretty URLs. 213 if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 214 $params = (array) $request->getParsedBody(); 215 216 return redirect(route(static::class, [ 217 'ancestors' => $params['ancestors'], 218 'recursion' => $params['recursion'], 219 'tree' => $tree->name(), 220 'xref' => $params['xref'], 221 'xref2' => $params['xref2'], 222 ])); 223 } 224 225 $individual1 = Factory::individual()->make($xref, $tree); 226 $individual2 = Factory::individual()->make($xref2, $tree); 227 228 $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS); 229 $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 230 231 $recursion = min($recursion, $max_recursion); 232 233 if ($individual1 instanceof Individual) { 234 $individual1 = Auth::checkIndividualAccess($individual1, false, true); 235 } 236 237 if ($individual2 instanceof Individual) { 238 $individual2 = Auth::checkIndividualAccess($individual2, false, true); 239 } 240 241 Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); 242 243 if ($individual1 instanceof Individual && $individual2 instanceof Individual) { 244 if ($ajax === '1') { 245 return $this->chart($individual1, $individual2, $recursion, $ancestors); 246 } 247 248 /* I18N: %s are individual’s names */ 249 $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName()); 250 $ajax_url = $this->chartUrl($individual1, [ 251 'ajax' => true, 252 'ancestors' => $ancestors, 253 'recursion' => $recursion, 254 'xref2' => $individual2->xref(), 255 ]); 256 } else { 257 $title = I18N::translate('Relationships'); 258 $ajax_url = ''; 259 } 260 261 return $this->viewResponse('modules/relationships-chart/page', [ 262 'ajax_url' => $ajax_url, 263 'ancestors' => $ancestors, 264 'ancestors_only' => $ancestors_only, 265 'ancestors_options' => $this->ancestorsOptions(), 266 'individual1' => $individual1, 267 'individual2' => $individual2, 268 'max_recursion' => $max_recursion, 269 'module' => $this->name(), 270 'recursion' => $recursion, 271 'recursion_options' => $this->recursionOptions($max_recursion), 272 'title' => $title, 273 'tree' => $tree, 274 ]); 275 } 276 277 /** 278 * @param Individual $individual1 279 * @param Individual $individual2 280 * @param int $recursion 281 * @param int $ancestors 282 * 283 * @return ResponseInterface 284 */ 285 public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface 286 { 287 $tree = $individual1->tree(); 288 289 $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 290 291 $recursion = min($recursion, $max_recursion); 292 293 $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); 294 295 ob_start(); 296 if (I18N::direction() === 'ltr') { 297 $diagonal1 = asset('css/images/dline.png'); 298 $diagonal2 = asset('css/images/dline2.png'); 299 } else { 300 $diagonal1 = asset('css/images/dline2.png'); 301 $diagonal2 = asset('css/images/dline.png'); 302 } 303 304 $num_paths = 0; 305 foreach ($paths as $path) { 306 // Extract the relationship names between pairs of individuals 307 $relationships = $this->oldStyleRelationshipPath($tree, $path); 308 if ($relationships === []) { 309 // Cannot see one of the families/individuals, due to privacy; 310 continue; 311 } 312 echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>'; 313 $num_paths++; 314 315 // Use a table/grid for layout. 316 $table = []; 317 // Current position in the grid. 318 $x = 0; 319 $y = 0; 320 // Extent of the grid. 321 $min_y = 0; 322 $max_y = 0; 323 $max_x = 0; 324 // For each node in the path. 325 foreach ($path as $n => $xref) { 326 if ($n % 2 === 1) { 327 switch ($relationships[$n]) { 328 case 'hus': 329 case 'wif': 330 case 'spo': 331 case 'bro': 332 case 'sis': 333 case 'sib': 334 $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center; width: 94px; text-align: center"><div class="hline-text" style="height: 32px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Factory::individual()->make($path[$n - 1], $tree), Factory::individual()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px;">' . view('icons/arrow-right') . '</div></div>'; 335 $x += 2; 336 break; 337 case 'son': 338 case 'dau': 339 case 'chi': 340 if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { 341 $table[$x + 1][$y - 1] = '<div style="background:url(' . $diagonal2 . '); width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: end;">' . Functions::getRelationshipNameFromPath($relationships[$n], Factory::individual()->make($path[$n - 1], $tree), Factory::individual()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: start;">' . view('icons/arrow-down') . '</div></div>'; 342 $x += 2; 343 } else { 344 $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div class="vline-text" style="display: inline-block; width:50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Factory::individual()->make($path[$n - 1], $tree), Factory::individual()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width:50%; line-height: 64px;">' . view('icons/arrow-down') . '</div></div>'; 345 } 346 $y -= 2; 347 break; 348 case 'fat': 349 case 'mot': 350 case 'par': 351 if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { 352 $table[$x + 1][$y + 1] = '<div style="background:url(' . $diagonal1 . '); background-position: top right; width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: start;">' . Functions::getRelationshipNameFromPath($relationships[$n], Factory::individual()->make($path[$n - 1], $tree), Factory::individual()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: end;">' . view('icons/arrow-down') . '</div></div>'; 353 $x += 2; 354 } else { 355 $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Factory::individual()->make($path[$n - 1], $tree), Factory::individual()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width: 50%; line-height: 32px">' . view('icons/arrow-up') . '</div></div>'; 356 } 357 $y += 2; 358 break; 359 } 360 $max_x = max($max_x, $x); 361 $min_y = min($min_y, $y); 362 $max_y = max($max_y, $y); 363 } else { 364 $individual = Factory::individual()->make($xref, $tree); 365 $table[$x][$y] = view('chart-box', ['individual' => $individual]); 366 } 367 } 368 echo '<div class="wt-chart wt-chart-relationships">'; 369 echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; 370 for ($y = $max_y; $y >= $min_y; --$y) { 371 echo '<tr>'; 372 for ($x = 0; $x <= $max_x; ++$x) { 373 echo '<td style="padding: 0;">'; 374 if (isset($table[$x][$y])) { 375 echo $table[$x][$y]; 376 } 377 echo '</td>'; 378 } 379 echo '</tr>'; 380 } 381 echo '</table>'; 382 echo '</div>'; 383 } 384 385 if (!$num_paths) { 386 echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; 387 } 388 389 $html = ob_get_clean(); 390 391 return response($html); 392 } 393 394 /** 395 * @param ServerRequestInterface $request 396 * 397 * @return ResponseInterface 398 */ 399 public function getAdminAction(ServerRequestInterface $request): ResponseInterface 400 { 401 $this->layout = 'layouts/administration'; 402 403 return $this->viewResponse('modules/relationships-chart/config', [ 404 'all_trees' => $this->tree_service->all(), 405 'ancestors_options' => $this->ancestorsOptions(), 406 'default_ancestors' => self::DEFAULT_ANCESTORS, 407 'default_recursion' => self::DEFAULT_RECURSION, 408 'recursion_options' => $this->recursionConfigOptions(), 409 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), 410 ]); 411 } 412 413 /** 414 * @param ServerRequestInterface $request 415 * 416 * @return ResponseInterface 417 */ 418 public function postAdminAction(ServerRequestInterface $request): ResponseInterface 419 { 420 $params = (array) $request->getParsedBody(); 421 422 foreach ($this->tree_service->all() as $tree) { 423 $recursion = $params['relationship-recursion-' . $tree->id()] ?? ''; 424 $ancestors = $params['relationship-ancestors-' . $tree->id()] ?? ''; 425 426 $tree->setPreference('RELATIONSHIP_RECURSION', $recursion); 427 $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors); 428 } 429 430 FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success'); 431 432 return redirect($this->getConfigLink()); 433 } 434 435 /** 436 * Possible options for the ancestors option 437 * 438 * @return string[] 439 */ 440 private function ancestorsOptions(): array 441 { 442 return [ 443 0 => I18N::translate('Find any relationship'), 444 1 => I18N::translate('Find relationships via ancestors'), 445 ]; 446 } 447 448 /** 449 * Possible options for the recursion option 450 * 451 * @return string[] 452 */ 453 private function recursionConfigOptions(): array 454 { 455 return [ 456 0 => I18N::translate('none'), 457 1 => I18N::number(1), 458 2 => I18N::number(2), 459 3 => I18N::number(3), 460 self::UNLIMITED_RECURSION => I18N::translate('unlimited'), 461 ]; 462 } 463 464 /** 465 * Calculate the shortest paths - or all paths - between two individuals. 466 * 467 * @param Individual $individual1 468 * @param Individual $individual2 469 * @param int $recursion How many levels of recursion to use 470 * @param bool $ancestor Restrict to relationships via a common ancestor 471 * 472 * @return string[][] 473 */ 474 private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array 475 { 476 $tree = $individual1->tree(); 477 478 $rows = DB::table('link') 479 ->where('l_file', '=', $tree->id()) 480 ->whereIn('l_type', ['FAMS', 'FAMC']) 481 ->select(['l_from', 'l_to']) 482 ->get(); 483 484 // Optionally restrict the graph to the ancestors of the individuals. 485 if ($ancestor) { 486 $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id()); 487 $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id()); 488 } else { 489 $ancestors = []; 490 $exclude = []; 491 } 492 493 $graph = []; 494 495 foreach ($rows as $row) { 496 if ($ancestors === [] || in_array($row->l_from, $ancestors, true) && !in_array($row->l_to, $exclude, true)) { 497 $graph[$row->l_from][$row->l_to] = 1; 498 $graph[$row->l_to][$row->l_from] = 1; 499 } 500 } 501 502 $xref1 = $individual1->xref(); 503 $xref2 = $individual2->xref(); 504 $dijkstra = new Dijkstra($graph); 505 $paths = $dijkstra->shortestPaths($xref1, $xref2); 506 507 // Only process each exclusion list once; 508 $excluded = []; 509 510 $queue = []; 511 foreach ($paths as $path) { 512 // Insert the paths into the queue, with an exclusion list. 513 $queue[] = [ 514 'path' => $path, 515 'exclude' => [], 516 ]; 517 // While there are un-extended paths 518 for ($next = current($queue); $next !== false; $next = next($queue)) { 519 // For each family on the path 520 for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { 521 $exclude = $next['exclude']; 522 if (count($exclude) >= $recursion) { 523 continue; 524 } 525 $exclude[] = $next['path'][$n]; 526 sort($exclude); 527 $tmp = implode('-', $exclude); 528 if (in_array($tmp, $excluded, true)) { 529 continue; 530 } 531 532 $excluded[] = $tmp; 533 // Add any new path to the queue 534 foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { 535 $queue[] = [ 536 'path' => $new_path, 537 'exclude' => $exclude, 538 ]; 539 } 540 } 541 } 542 } 543 // Extract the paths from the queue. 544 $paths = []; 545 foreach ($queue as $next) { 546 // The Dijkstra library does not use strict types, and converts 547 // numeric array keys (XREFs) from strings to integers; 548 $path = array_map($this->stringMapper(), $next['path']); 549 550 // Remove duplicates 551 $paths[implode('-', $next['path'])] = $path; 552 } 553 554 return $paths; 555 } 556 557 /** 558 * Convert numeric values to strings 559 * 560 * @return Closure 561 */ 562 private function stringMapper(): Closure 563 { 564 return static function ($xref) { 565 return (string) $xref; 566 }; 567 } 568 569 /** 570 * Find all ancestors of a list of individuals 571 * 572 * @param string $xref1 573 * @param string $xref2 574 * @param int $tree_id 575 * 576 * @return string[] 577 */ 578 private function allAncestors($xref1, $xref2, $tree_id): array 579 { 580 $ancestors = [ 581 $xref1, 582 $xref2, 583 ]; 584 585 $queue = [ 586 $xref1, 587 $xref2, 588 ]; 589 while ($queue !== []) { 590 $parents = DB::table('link AS l1') 591 ->join('link AS l2', static function (JoinClause $join): void { 592 $join 593 ->on('l1.l_to', '=', 'l2.l_to') 594 ->on('l1.l_file', '=', 'l2.l_file'); 595 }) 596 ->where('l1.l_file', '=', $tree_id) 597 ->where('l1.l_type', '=', 'FAMC') 598 ->where('l2.l_type', '=', 'FAMS') 599 ->whereIn('l1.l_from', $queue) 600 ->pluck('l2.l_from'); 601 602 $queue = []; 603 foreach ($parents as $parent) { 604 if (!in_array($parent, $ancestors, true)) { 605 $ancestors[] = $parent; 606 $queue[] = $parent; 607 } 608 } 609 } 610 611 return $ancestors; 612 } 613 614 /** 615 * Find all families of two individuals 616 * 617 * @param string $xref1 618 * @param string $xref2 619 * @param int $tree_id 620 * 621 * @return string[] 622 */ 623 private function excludeFamilies($xref1, $xref2, $tree_id): array 624 { 625 return DB::table('link AS l1') 626 ->join('link AS l2', static function (JoinClause $join): void { 627 $join 628 ->on('l1.l_to', '=', 'l2.l_to') 629 ->on('l1.l_type', '=', 'l2.l_type') 630 ->on('l1.l_file', '=', 'l2.l_file'); 631 }) 632 ->where('l1.l_file', '=', $tree_id) 633 ->where('l1.l_type', '=', 'FAMS') 634 ->where('l1.l_from', '=', $xref1) 635 ->where('l2.l_from', '=', $xref2) 636 ->pluck('l1.l_to') 637 ->all(); 638 } 639 640 /** 641 * Convert a path (list of XREFs) to an "old-style" string of relationships. 642 * Return an empty array, if privacy rules prevent us viewing any node. 643 * 644 * @param Tree $tree 645 * @param string[] $path Alternately Individual / Family 646 * 647 * @return string[] 648 */ 649 private function oldStyleRelationshipPath(Tree $tree, array $path): array 650 { 651 $spouse_codes = [ 652 'M' => 'hus', 653 'F' => 'wif', 654 'U' => 'spo', 655 ]; 656 $parent_codes = [ 657 'M' => 'fat', 658 'F' => 'mot', 659 'U' => 'par', 660 ]; 661 $child_codes = [ 662 'M' => 'son', 663 'F' => 'dau', 664 'U' => 'chi', 665 ]; 666 $sibling_codes = [ 667 'M' => 'bro', 668 'F' => 'sis', 669 'U' => 'sib', 670 ]; 671 $relationships = []; 672 673 for ($i = 1, $count = count($path); $i < $count; $i += 2) { 674 $family = Factory::family()->make($path[$i], $tree); 675 $prev = Factory::individual()->make($path[$i - 1], $tree); 676 $next = Factory::individual()->make($path[$i + 1], $tree); 677 if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { 678 $rel1 = $match[1]; 679 } else { 680 return []; 681 } 682 if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { 683 $rel2 = $match[1]; 684 } else { 685 return []; 686 } 687 if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 688 $relationships[$i] = $spouse_codes[$next->sex()]; 689 } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { 690 $relationships[$i] = $child_codes[$next->sex()]; 691 } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 692 $relationships[$i] = $parent_codes[$next->sex()]; 693 } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { 694 $relationships[$i] = $sibling_codes[$next->sex()]; 695 } 696 } 697 698 return $relationships; 699 } 700 701 /** 702 * Possible options for the recursion option 703 * 704 * @param int $max_recursion 705 * 706 * @return string[] 707 */ 708 private function recursionOptions(int $max_recursion): array 709 { 710 if ($max_recursion === static::UNLIMITED_RECURSION) { 711 $text = I18N::translate('Find all possible relationships'); 712 } else { 713 $text = I18N::translate('Find other relationships'); 714 } 715 716 return [ 717 '0' => I18N::translate('Find the closest relationships'), 718 $max_recursion => $text, 719 ]; 720 } 721} 722