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