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