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