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