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