xref: /webtrees/app/Module/RelationshipsChartModule.php (revision 3c3fd0a55e7d63bbaf92e0ce4073276aba583972)
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