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