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