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