xref: /webtrees/app/Module/RelationshipsChartModule.php (revision 1ff45046fabc22237b5d0d8e489c96f031fc598d)
1168ff6f3Sric2016<?php
23976b470SGreg Roach
3168ff6f3Sric2016/**
4168ff6f3Sric2016 * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify
7168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by
8168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or
9168ff6f3Sric2016 * (at your option) any later version.
10168ff6f3Sric2016 * This program is distributed in the hope that it will be useful,
11168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13168ff6f3Sric2016 * GNU General Public License for more details.
14168ff6f3Sric2016 * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16168ff6f3Sric2016 */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module;
21168ff6f3Sric2016
22185cbb4dSGreg Roachuse Closure;
233cfcc809SGreg Roachuse Fig\Http\Message\RequestMethodInterface;
249b5537c3SGreg Roachuse Fisharebest\Algorithm\Dijkstra;
25168ff6f3Sric2016use Fisharebest\Webtrees\Auth;
261fe542e9SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
276f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
2845ac604bSGreg Roachuse Fisharebest\Webtrees\FlashMessages;
296fcafd02SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
30168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
31168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
321e3273c9SGreg Roachuse Fisharebest\Webtrees\Menu;
336fcafd02SGreg Roachuse Fisharebest\Webtrees\Registry;
346fcafd02SGreg Roachuse Fisharebest\Webtrees\Services\RelationshipService;
353df1e584SGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
3645ac604bSGreg Roachuse Fisharebest\Webtrees\Tree;
37b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
389b5537c3SGreg Roachuse Illuminate\Database\Query\JoinClause;
396fcafd02SGreg Roachuse Illuminate\Support\Collection;
406ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
416ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
423cfcc809SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
433976b470SGreg Roach
44c5c278deSGreg Roachuse function array_map;
45c5c278deSGreg Roachuse function asset;
4610e06497SGreg Roachuse function count;
47c5c278deSGreg Roachuse function current;
48c5c278deSGreg Roachuse function e;
49c5c278deSGreg Roachuse function implode;
5010e06497SGreg Roachuse function in_array;
51c5c278deSGreg Roachuse function max;
52c5c278deSGreg Roachuse function min;
53c5c278deSGreg Roachuse function next;
54c5c278deSGreg Roachuse function ob_get_clean;
55c5c278deSGreg Roachuse function ob_start;
56c5c278deSGreg Roachuse function preg_match;
573cfcc809SGreg Roachuse function redirect;
58c5c278deSGreg Roachuse function response;
593cfcc809SGreg Roachuse function route;
60c5c278deSGreg Roachuse function sort;
61f4ba05e3SGreg Roachuse function view;
62168ff6f3Sric2016
63168ff6f3Sric2016/**
64168ff6f3Sric2016 * Class RelationshipsChartModule
65168ff6f3Sric2016 */
667a1b7425SGreg Roachclass RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, ModuleConfigInterface, RequestHandlerInterface
67c1010edaSGreg Roach{
6849a243cbSGreg Roach    use ModuleChartTrait;
6949a243cbSGreg Roach    use ModuleConfigTrait;
7049a243cbSGreg Roach
7172f04adfSGreg Roach    protected const ROUTE_URL = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}';
723cfcc809SGreg Roach
731e3273c9SGreg Roach    /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */
7416d6367aSGreg Roach    public const UNLIMITED_RECURSION = 99;
751e3273c9SGreg Roach
761e3273c9SGreg Roach    /** By default new trees allow unlimited recursion */
7716d6367aSGreg Roach    public const DEFAULT_RECURSION = '99';
7845ac604bSGreg Roach
79e0bd7dc9SGreg Roach    /** By default new trees search for all relationships (not via ancestors) */
8016d6367aSGreg Roach    public const DEFAULT_ANCESTORS  = '0';
813cfcc809SGreg Roach    public const DEFAULT_PARAMETERS = [
823cfcc809SGreg Roach        'ancestors' => self::DEFAULT_ANCESTORS,
833cfcc809SGreg Roach        'recursion' => self::DEFAULT_RECURSION,
843cfcc809SGreg Roach    ];
853cfcc809SGreg Roach
866fcafd02SGreg Roach    private TreeService $tree_service;
876fcafd02SGreg Roach
886fcafd02SGreg Roach    private RelationshipService $relationship_service;
893df1e584SGreg Roach
903df1e584SGreg Roach    /**
916fcafd02SGreg Roach     * @param RelationshipService $relationship_service
923df1e584SGreg Roach     * @param TreeService         $tree_service
933df1e584SGreg Roach     */
94c5c278deSGreg Roach    public function __construct(RelationshipService $relationship_service, TreeService $tree_service)
953df1e584SGreg Roach    {
966fcafd02SGreg Roach        $this->relationship_service = $relationship_service;
973df1e584SGreg Roach        $this->tree_service         = $tree_service;
983df1e584SGreg Roach    }
993df1e584SGreg Roach
1003cfcc809SGreg Roach    /**
1013cfcc809SGreg Roach     * Initialization.
1023cfcc809SGreg Roach     *
1039e18e23bSGreg Roach     * @return void
1043cfcc809SGreg Roach     */
1059e18e23bSGreg Roach    public function boot(): void
1063cfcc809SGreg Roach    {
107158900c2SGreg Roach        Registry::routeFactory()->routeMap()
10872f04adfSGreg Roach            ->get(static::class, static::ROUTE_URL, $this)
1093cfcc809SGreg Roach            ->allows(RequestMethodInterface::METHOD_POST)
1103cfcc809SGreg Roach            ->tokens([
1113cfcc809SGreg Roach                'ancestors' => '\d+',
1123cfcc809SGreg Roach                'recursion' => '\d+',
1133cfcc809SGreg Roach            ]);
1143cfcc809SGreg Roach    }
115e0bd7dc9SGreg Roach
116168ff6f3Sric2016    /**
117168ff6f3Sric2016     * A sentence describing what this module does.
118168ff6f3Sric2016     *
119168ff6f3Sric2016     * @return string
120168ff6f3Sric2016     */
12149a243cbSGreg Roach    public function description(): string
122c1010edaSGreg Roach    {
123bbb76c12SGreg Roach        /* I18N: Description of the “RelationshipsChart” module */
124bbb76c12SGreg Roach        return I18N::translate('A chart displaying relationships between two individuals.');
125168ff6f3Sric2016    }
126168ff6f3Sric2016
127168ff6f3Sric2016    /**
1286ccdf4f0SGreg Roach     * Return a menu item for this chart - for use in individual boxes.
1296ccdf4f0SGreg Roach     *
1306ccdf4f0SGreg Roach     * @param Individual $individual
1316ccdf4f0SGreg Roach     *
1326ccdf4f0SGreg Roach     * @return Menu|null
1336ccdf4f0SGreg Roach     */
134*1ff45046SGreg Roach    public function chartBoxMenu(Individual $individual): Menu|null
1356ccdf4f0SGreg Roach    {
1366ccdf4f0SGreg Roach        return $this->chartMenu($individual);
1376ccdf4f0SGreg Roach    }
1386ccdf4f0SGreg Roach
1396ccdf4f0SGreg Roach    /**
140e6562982SGreg Roach     * A main menu item for this chart.
141168ff6f3Sric2016     *
1428e69695bSGreg Roach     * @param Individual $individual
1438e69695bSGreg Roach     *
144e6562982SGreg Roach     * @return Menu
145168ff6f3Sric2016     */
146e6562982SGreg Roach    public function chartMenu(Individual $individual): Menu
147c1010edaSGreg Roach    {
148a0c3c04bSGreg Roach        $my_xref = $individual->tree()->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF);
149168ff6f3Sric2016
150a0c3c04bSGreg Roach        if ($my_xref !== '' && $my_xref !== $individual->xref()) {
151a0c3c04bSGreg Roach            $my_record = Registry::individualFactory()->make($my_xref, $individual->tree());
152a0c3c04bSGreg Roach
1532491533aSGreg Roach            if ($my_record instanceof Individual) {
154168ff6f3Sric2016                return new Menu(
155168ff6f3Sric2016                    I18N::translate('Relationship to me'),
156a0c3c04bSGreg Roach                    $this->chartUrl($my_record, ['xref2' => $individual->xref()]),
157377a2979SGreg Roach                    $this->chartMenuClass(),
158e6562982SGreg Roach                    $this->chartUrlAttributes()
159168ff6f3Sric2016                );
160b2ce94c6SRico Sonntag            }
1612491533aSGreg Roach        }
162b2ce94c6SRico Sonntag
163168ff6f3Sric2016        return new Menu(
164e6562982SGreg Roach            $this->title(),
165e6562982SGreg Roach            $this->chartUrl($individual),
166377a2979SGreg Roach            $this->chartMenuClass(),
167e6562982SGreg Roach            $this->chartUrlAttributes()
168168ff6f3Sric2016        );
169168ff6f3Sric2016    }
170168ff6f3Sric2016
1714eb71cfaSGreg Roach    /**
172377a2979SGreg Roach     * CSS class for the URL.
173377a2979SGreg Roach     *
174377a2979SGreg Roach     * @return string
175377a2979SGreg Roach     */
176377a2979SGreg Roach    public function chartMenuClass(): string
177377a2979SGreg Roach    {
178377a2979SGreg Roach        return 'menu-chart-relationship';
179377a2979SGreg Roach    }
180377a2979SGreg Roach
181377a2979SGreg Roach    /**
1826ccdf4f0SGreg Roach     * How should this module be identified in the control panel, etc.?
1834eb71cfaSGreg Roach     *
1846ccdf4f0SGreg Roach     * @return string
1854eb71cfaSGreg Roach     */
1866ccdf4f0SGreg Roach    public function title(): string
187c1010edaSGreg Roach    {
1886ccdf4f0SGreg Roach        /* I18N: Name of a module/chart */
1896ccdf4f0SGreg Roach        return I18N::translate('Relationships');
190e6562982SGreg Roach    }
191e6562982SGreg Roach
192e6562982SGreg Roach    /**
1933cfcc809SGreg Roach     * The URL for a page showing chart options.
19457ab2231SGreg Roach     *
1953cfcc809SGreg Roach     * @param Individual                                $individual
19676d39c55SGreg Roach     * @param array<bool|int|string|array<string>|null> $parameters
19718d7a90dSGreg Roach     *
1983cfcc809SGreg Roach     * @return string
199e0bd7dc9SGreg Roach     */
2003cfcc809SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
201c1010edaSGreg Roach    {
20272f04adfSGreg Roach        return route(static::class, [
2033cfcc809SGreg Roach                'xref' => $individual->xref(),
2043cfcc809SGreg Roach                'tree' => $individual->tree()->name(),
2053cfcc809SGreg Roach            ] + $parameters + self::DEFAULT_PARAMETERS);
2061e3273c9SGreg Roach    }
2079b5537c3SGreg Roach
2089b5537c3SGreg Roach    /**
2096ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
2106ccdf4f0SGreg Roach     *
2116ccdf4f0SGreg Roach     * @return ResponseInterface
2126ccdf4f0SGreg Roach     */
2133cfcc809SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
2146ccdf4f0SGreg Roach    {
215b55cbc6bSGreg Roach        $tree      = Validator::attributes($request)->tree();
216b55cbc6bSGreg Roach        $xref      = Validator::attributes($request)->isXref()->string('xref');
217b55cbc6bSGreg Roach        $xref2     = Validator::attributes($request)->isXref()->string('xref2', '');
218b55cbc6bSGreg Roach        $ajax      = Validator::queryParams($request)->boolean('ajax', false);
2193cfcc809SGreg Roach        $ancestors = (int) $request->getAttribute('ancestors');
2203cfcc809SGreg Roach        $recursion = (int) $request->getAttribute('recursion');
221b55cbc6bSGreg Roach        $user      = Validator::attributes($request)->user();
2229b5537c3SGreg Roach
2233cfcc809SGreg Roach        // Convert POST requests into GET requests for pretty URLs.
2243cfcc809SGreg Roach        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
22572f04adfSGreg Roach            return redirect(route(static::class, [
2264ea62551SGreg Roach                'tree'      => $tree->name(),
227b55cbc6bSGreg Roach                'ancestors' => Validator::parsedBody($request)->string('ancestors', ''),
228b55cbc6bSGreg Roach                'recursion' => Validator::parsedBody($request)->string('recursion', ''),
229b55cbc6bSGreg Roach                'xref'      => Validator::parsedBody($request)->string('xref', ''),
230b55cbc6bSGreg Roach                'xref2'     => Validator::parsedBody($request)->string('xref2', ''),
2313cfcc809SGreg Roach            ]));
2323cfcc809SGreg Roach        }
2339b5537c3SGreg Roach
2346b9cb339SGreg Roach        $individual1 = Registry::individualFactory()->make($xref, $tree);
2356b9cb339SGreg Roach        $individual2 = Registry::individualFactory()->make($xref2, $tree);
2369b5537c3SGreg Roach
2373dcc812bSGreg Roach        $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS);
2383dcc812bSGreg Roach        $max_recursion  = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
2399b5537c3SGreg Roach
2409b5537c3SGreg Roach        $recursion = min($recursion, $max_recursion);
2419b5537c3SGreg Roach
242b55cbc6bSGreg Roach        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
243b55cbc6bSGreg Roach
2443dcc812bSGreg Roach        if ($individual1 instanceof Individual) {
2453c3fd0a5SGreg Roach            $individual1 = Auth::checkIndividualAccess($individual1, false, true);
2463dcc812bSGreg Roach        }
2473dcc812bSGreg Roach
2483dcc812bSGreg Roach        if ($individual2 instanceof Individual) {
2493c3fd0a5SGreg Roach            $individual2 = Auth::checkIndividualAccess($individual2, false, true);
2508365f910SGreg Roach        }
2513dcc812bSGreg Roach
2529b5537c3SGreg Roach        if ($individual1 instanceof Individual && $individual2 instanceof Individual) {
253b55cbc6bSGreg Roach            if ($ajax) {
254f866a2aeSGreg Roach                return $this->chart($individual1, $individual2, $recursion, $ancestors);
255f866a2aeSGreg Roach            }
256f866a2aeSGreg Roach
2579b5537c3SGreg Roach            /* I18N: %s are individual’s names */
25839ca88baSGreg Roach            $title    = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName());
2599b5537c3SGreg Roach            $ajax_url = $this->chartUrl($individual1, [
2609b5537c3SGreg Roach                'ajax'      => true,
2619b5537c3SGreg Roach                'ancestors' => $ancestors,
2623cfcc809SGreg Roach                'recursion' => $recursion,
2633cfcc809SGreg Roach                'xref2'     => $individual2->xref(),
2649b5537c3SGreg Roach            ]);
2653dcc812bSGreg Roach        } else {
2663dcc812bSGreg Roach            $title    = I18N::translate('Relationships');
267f866a2aeSGreg Roach            $ajax_url = '';
2683dcc812bSGreg Roach        }
2699b5537c3SGreg Roach
2709b5537c3SGreg Roach        return $this->viewResponse('modules/relationships-chart/page', [
2719b5537c3SGreg Roach            'ajax_url'          => $ajax_url,
2729b5537c3SGreg Roach            'ancestors'         => $ancestors,
2739b5537c3SGreg Roach            'ancestors_only'    => $ancestors_only,
2749b5537c3SGreg Roach            'ancestors_options' => $this->ancestorsOptions(),
2759b5537c3SGreg Roach            'individual1'       => $individual1,
2769b5537c3SGreg Roach            'individual2'       => $individual2,
2779b5537c3SGreg Roach            'max_recursion'     => $max_recursion,
27871378461SGreg Roach            'module'            => $this->name(),
2799b5537c3SGreg Roach            'recursion'         => $recursion,
2809b5537c3SGreg Roach            'recursion_options' => $this->recursionOptions($max_recursion),
2819b5537c3SGreg Roach            'title'             => $title,
2823cfcc809SGreg Roach            'tree'              => $tree,
2839b5537c3SGreg Roach        ]);
2849b5537c3SGreg Roach    }
2859b5537c3SGreg Roach
2869b5537c3SGreg Roach    /**
2873dcc812bSGreg Roach     * @param Individual $individual1
2883dcc812bSGreg Roach     * @param Individual $individual2
2893dcc812bSGreg Roach     * @param int        $recursion
2903dcc812bSGreg Roach     * @param int        $ancestors
2919b5537c3SGreg Roach     *
2926ccdf4f0SGreg Roach     * @return ResponseInterface
2939b5537c3SGreg Roach     */
2946ccdf4f0SGreg Roach    public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface
2959b5537c3SGreg Roach    {
2963dcc812bSGreg Roach        $tree = $individual1->tree();
2979b5537c3SGreg Roach
2983dcc812bSGreg Roach        $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
2999b5537c3SGreg Roach
3009b5537c3SGreg Roach        $recursion = min($recursion, $max_recursion);
3019b5537c3SGreg Roach
3029b5537c3SGreg Roach        $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors);
3039b5537c3SGreg Roach
3049b5537c3SGreg Roach        ob_start();
3059b5537c3SGreg Roach        if (I18N::direction() === 'ltr') {
306e837ff07SGreg Roach            $diagonal1 = asset('css/images/dline.png');
307e837ff07SGreg Roach            $diagonal2 = asset('css/images/dline2.png');
3089b5537c3SGreg Roach        } else {
309e837ff07SGreg Roach            $diagonal1 = asset('css/images/dline2.png');
310e837ff07SGreg Roach            $diagonal2 = asset('css/images/dline.png');
3119b5537c3SGreg Roach        }
3129b5537c3SGreg Roach
3139b5537c3SGreg Roach        $num_paths = 0;
3149b5537c3SGreg Roach        foreach ($paths as $path) {
3159b5537c3SGreg Roach            // Extract the relationship names between pairs of individuals
3169b5537c3SGreg Roach            $relationships = $this->oldStyleRelationshipPath($tree, $path);
317075d1a05SGreg Roach            if ($relationships === []) {
3189b5537c3SGreg Roach                // Cannot see one of the families/individuals, due to privacy;
3199b5537c3SGreg Roach                continue;
3209b5537c3SGreg Roach            }
3216fcafd02SGreg Roach
3226fcafd02SGreg Roach            $nodes = Collection::make($path)
3236fcafd02SGreg Roach                ->map(static function (string $xref, int $key) use ($tree): GedcomRecord {
3246fcafd02SGreg Roach                    if ($key % 2 === 0) {
3256fcafd02SGreg Roach                        return Registry::individualFactory()->make($xref, $tree);
3266fcafd02SGreg Roach                    }
3276fcafd02SGreg Roach
3286fcafd02SGreg Roach                    return  Registry::familyFactory()->make($xref, $tree);
3296fcafd02SGreg Roach                });
3306fcafd02SGreg Roach
331c5c278deSGreg Roach            $relationship = $this->relationship_service->nameFromPath($nodes->all(), I18N::language());
3326fcafd02SGreg Roach
333c5c278deSGreg Roach            echo '<h3>', I18N::translate('Relationship: %s', $relationship), '</h3>';
334c5c278deSGreg Roach
3359b5537c3SGreg Roach            $num_paths++;
3369b5537c3SGreg Roach
3379b5537c3SGreg Roach            // Use a table/grid for layout.
3389b5537c3SGreg Roach            $table = [];
3399b5537c3SGreg Roach            // Current position in the grid.
3409b5537c3SGreg Roach            $x = 0;
3419b5537c3SGreg Roach            $y = 0;
3429b5537c3SGreg Roach            // Extent of the grid.
3439b5537c3SGreg Roach            $min_y = 0;
3449b5537c3SGreg Roach            $max_y = 0;
3459b5537c3SGreg Roach            $max_x = 0;
3469b5537c3SGreg Roach            // For each node in the path.
3479b5537c3SGreg Roach            foreach ($path as $n => $xref) {
3489b5537c3SGreg Roach                if ($n % 2 === 1) {
3499b5537c3SGreg Roach                    switch ($relationships[$n]) {
3509b5537c3SGreg Roach                        case 'hus':
3519b5537c3SGreg Roach                        case 'wif':
3529b5537c3SGreg Roach                        case 'spo':
3539b5537c3SGreg Roach                        case 'bro':
3549b5537c3SGreg Roach                        case 'sis':
3559b5537c3SGreg Roach                        case 'sib':
3569c259058SGreg Roach                            $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center;  width: 94px; text-align: center"><div 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>';
3579b5537c3SGreg Roach                            $x += 2;
3589b5537c3SGreg Roach                            break;
3599b5537c3SGreg Roach                        case 'son':
3609b5537c3SGreg Roach                        case 'dau':
3619b5537c3SGreg Roach                        case 'chi':
3629b5537c3SGreg Roach                            if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) {
363b55cbc6bSGreg Roach                                $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>';
3649b5537c3SGreg Roach                                $x += 2;
3659b5537c3SGreg Roach                            } else {
3669c259058SGreg Roach                                $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div 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>';
3679b5537c3SGreg Roach                            }
3689b5537c3SGreg Roach                            $y -= 2;
3699b5537c3SGreg Roach                            break;
3709b5537c3SGreg Roach                        case 'fat':
3719b5537c3SGreg Roach                        case 'mot':
3729b5537c3SGreg Roach                        case 'par':
3739b5537c3SGreg Roach                            if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) {
374b55cbc6bSGreg Roach                                $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>';
3759b5537c3SGreg Roach                                $x += 2;
3769b5537c3SGreg Roach                            } else {
3779c259058SGreg Roach                                $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div 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>';
3789b5537c3SGreg Roach                            }
3799b5537c3SGreg Roach                            $y += 2;
3809b5537c3SGreg Roach                            break;
3819b5537c3SGreg Roach                    }
3829b5537c3SGreg Roach                    $max_x = max($max_x, $x);
3839b5537c3SGreg Roach                    $min_y = min($min_y, $y);
3849b5537c3SGreg Roach                    $max_y = max($max_y, $y);
3859b5537c3SGreg Roach                } else {
3866b9cb339SGreg Roach                    $individual    = Registry::individualFactory()->make($xref, $tree);
387f4ba05e3SGreg Roach                    $table[$x][$y] = view('chart-box', ['individual' => $individual]);
3889b5537c3SGreg Roach                }
3899b5537c3SGreg Roach            }
3904a2590a5SGreg Roach            echo '<div class="wt-chart wt-chart-relationships">';
3919b5537c3SGreg Roach            echo '<table style="border-collapse: collapse; margin: 20px 50px;">';
3929b5537c3SGreg Roach            for ($y = $max_y; $y >= $min_y; --$y) {
3939b5537c3SGreg Roach                echo '<tr>';
3949b5537c3SGreg Roach                for ($x = 0; $x <= $max_x; ++$x) {
3959b5537c3SGreg Roach                    echo '<td style="padding: 0;">';
3969b5537c3SGreg Roach                    if (isset($table[$x][$y])) {
3979b5537c3SGreg Roach                        echo $table[$x][$y];
3989b5537c3SGreg Roach                    }
3999b5537c3SGreg Roach                    echo '</td>';
4009b5537c3SGreg Roach                }
4019b5537c3SGreg Roach                echo '</tr>';
4029b5537c3SGreg Roach            }
4039b5537c3SGreg Roach            echo '</table>';
4049b5537c3SGreg Roach            echo '</div>';
4059b5537c3SGreg Roach        }
4069b5537c3SGreg Roach
4079b5537c3SGreg Roach        if (!$num_paths) {
4089b5537c3SGreg Roach            echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>';
4099b5537c3SGreg Roach        }
4109b5537c3SGreg Roach
4119b5537c3SGreg Roach        $html = ob_get_clean();
4129b5537c3SGreg Roach
4136ccdf4f0SGreg Roach        return response($html);
4149b5537c3SGreg Roach    }
4159b5537c3SGreg Roach
4169b5537c3SGreg Roach    /**
4173cfcc809SGreg Roach     * @param ServerRequestInterface $request
4183cfcc809SGreg Roach     *
4193cfcc809SGreg Roach     * @return ResponseInterface
4203cfcc809SGreg Roach     */
4213cfcc809SGreg Roach    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
4223cfcc809SGreg Roach    {
4233cfcc809SGreg Roach        $this->layout = 'layouts/administration';
4243cfcc809SGreg Roach
4253cfcc809SGreg Roach        return $this->viewResponse('modules/relationships-chart/config', [
4263df1e584SGreg Roach            'all_trees'         => $this->tree_service->all(),
4273cfcc809SGreg Roach            'ancestors_options' => $this->ancestorsOptions(),
4283cfcc809SGreg Roach            'default_ancestors' => self::DEFAULT_ANCESTORS,
4293cfcc809SGreg Roach            'default_recursion' => self::DEFAULT_RECURSION,
4303cfcc809SGreg Roach            'recursion_options' => $this->recursionConfigOptions(),
4313cfcc809SGreg Roach            'title'             => I18N::translate('Chart preferences') . ' — ' . $this->title(),
4323cfcc809SGreg Roach        ]);
4333cfcc809SGreg Roach    }
4343cfcc809SGreg Roach
4353cfcc809SGreg Roach    /**
4363cfcc809SGreg Roach     * @param ServerRequestInterface $request
4373cfcc809SGreg Roach     *
4383cfcc809SGreg Roach     * @return ResponseInterface
4393cfcc809SGreg Roach     */
4403cfcc809SGreg Roach    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
4413cfcc809SGreg Roach    {
4423df1e584SGreg Roach        foreach ($this->tree_service->all() as $tree) {
443748dbe15SGreg Roach            $recursion = Validator::parsedBody($request)->integer('relationship-recursion-' . $tree->id());
444748dbe15SGreg Roach            $ancestors = Validator::parsedBody($request)->string('relationship-ancestors-' . $tree->id());
4453cfcc809SGreg Roach
446748dbe15SGreg Roach            $tree->setPreference('RELATIONSHIP_RECURSION', (string) $recursion);
4473cfcc809SGreg Roach            $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors);
4483cfcc809SGreg Roach        }
4493cfcc809SGreg Roach
4503cfcc809SGreg Roach        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
4513cfcc809SGreg Roach
4523cfcc809SGreg Roach        return redirect($this->getConfigLink());
4533cfcc809SGreg Roach    }
4543cfcc809SGreg Roach
4553cfcc809SGreg Roach    /**
4563cfcc809SGreg Roach     * Possible options for the ancestors option
4573cfcc809SGreg Roach     *
4587c2c99faSGreg Roach     * @return array<int,string>
4593cfcc809SGreg Roach     */
4603cfcc809SGreg Roach    private function ancestorsOptions(): array
4613cfcc809SGreg Roach    {
4623cfcc809SGreg Roach        return [
4633cfcc809SGreg Roach            0 => I18N::translate('Find any relationship'),
4643cfcc809SGreg Roach            1 => I18N::translate('Find relationships via ancestors'),
4653cfcc809SGreg Roach        ];
4663cfcc809SGreg Roach    }
4673cfcc809SGreg Roach
4683cfcc809SGreg Roach    /**
4693cfcc809SGreg Roach     * Possible options for the recursion option
4703cfcc809SGreg Roach     *
4717c2c99faSGreg Roach     * @return array<int,string>
4723cfcc809SGreg Roach     */
4733cfcc809SGreg Roach    private function recursionConfigOptions(): array
4743cfcc809SGreg Roach    {
4753cfcc809SGreg Roach        return [
4763cfcc809SGreg Roach            0                         => I18N::translate('none'),
4773cfcc809SGreg Roach            1                         => I18N::number(1),
4783cfcc809SGreg Roach            2                         => I18N::number(2),
4793cfcc809SGreg Roach            3                         => I18N::number(3),
4803cfcc809SGreg Roach            self::UNLIMITED_RECURSION => I18N::translate('unlimited'),
4813cfcc809SGreg Roach        ];
4823cfcc809SGreg Roach    }
4833cfcc809SGreg Roach
4843cfcc809SGreg Roach    /**
4859b5537c3SGreg Roach     * Calculate the shortest paths - or all paths - between two individuals.
4869b5537c3SGreg Roach     *
4879b5537c3SGreg Roach     * @param Individual $individual1
4889b5537c3SGreg Roach     * @param Individual $individual2
4899b5537c3SGreg Roach     * @param int        $recursion How many levels of recursion to use
4909b5537c3SGreg Roach     * @param bool       $ancestor  Restrict to relationships via a common ancestor
4919b5537c3SGreg Roach     *
49224f2a3afSGreg Roach     * @return array<array<string>>
4939b5537c3SGreg Roach     */
49424f2a3afSGreg Roach    private function calculateRelationships(
49524f2a3afSGreg Roach        Individual $individual1,
49624f2a3afSGreg Roach        Individual $individual2,
49724f2a3afSGreg Roach        int $recursion,
49824f2a3afSGreg Roach        bool $ancestor = false
49924f2a3afSGreg Roach    ): array {
5003dcc812bSGreg Roach        $tree = $individual1->tree();
5013dcc812bSGreg Roach
5029b5537c3SGreg Roach        $rows = DB::table('link')
5033dcc812bSGreg Roach            ->where('l_file', '=', $tree->id())
5049b5537c3SGreg Roach            ->whereIn('l_type', ['FAMS', 'FAMC'])
5059b5537c3SGreg Roach            ->select(['l_from', 'l_to'])
5069b5537c3SGreg Roach            ->get();
5079b5537c3SGreg Roach
5089b5537c3SGreg Roach        // Optionally restrict the graph to the ancestors of the individuals.
5099b5537c3SGreg Roach        if ($ancestor) {
5103dcc812bSGreg Roach            $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id());
5113dcc812bSGreg Roach            $exclude   = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id());
5129b5537c3SGreg Roach        } else {
5139b5537c3SGreg Roach            $ancestors = [];
5149b5537c3SGreg Roach            $exclude   = [];
5159b5537c3SGreg Roach        }
5169b5537c3SGreg Roach
5179b5537c3SGreg Roach        $graph = [];
5189b5537c3SGreg Roach
5199b5537c3SGreg Roach        foreach ($rows as $row) {
520075d1a05SGreg Roach            if ($ancestors === [] || in_array($row->l_from, $ancestors, true) && !in_array($row->l_to, $exclude, true)) {
5219b5537c3SGreg Roach                $graph[$row->l_from][$row->l_to] = 1;
5229b5537c3SGreg Roach                $graph[$row->l_to][$row->l_from] = 1;
5239b5537c3SGreg Roach            }
5249b5537c3SGreg Roach        }
5259b5537c3SGreg Roach
5269b5537c3SGreg Roach        $xref1    = $individual1->xref();
5279b5537c3SGreg Roach        $xref2    = $individual2->xref();
5289b5537c3SGreg Roach        $dijkstra = new Dijkstra($graph);
5299b5537c3SGreg Roach        $paths    = $dijkstra->shortestPaths($xref1, $xref2);
5309b5537c3SGreg Roach
5319b5537c3SGreg Roach        // Only process each exclusion list once;
5329b5537c3SGreg Roach        $excluded = [];
5339b5537c3SGreg Roach
5349b5537c3SGreg Roach        $queue = [];
5359b5537c3SGreg Roach        foreach ($paths as $path) {
5369b5537c3SGreg Roach            // Insert the paths into the queue, with an exclusion list.
5379b5537c3SGreg Roach            $queue[] = [
5389b5537c3SGreg Roach                'path'    => $path,
5399b5537c3SGreg Roach                'exclude' => [],
5409b5537c3SGreg Roach            ];
5419b5537c3SGreg Roach            // While there are un-extended paths
5429b5537c3SGreg Roach            for ($next = current($queue); $next !== false; $next = next($queue)) {
5439b5537c3SGreg Roach                // For each family on the path
5449b5537c3SGreg Roach                for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) {
5459b5537c3SGreg Roach                    $exclude = $next['exclude'];
5469b5537c3SGreg Roach                    if (count($exclude) >= $recursion) {
5479b5537c3SGreg Roach                        continue;
5489b5537c3SGreg Roach                    }
5499b5537c3SGreg Roach                    $exclude[] = $next['path'][$n];
5509b5537c3SGreg Roach                    sort($exclude);
5519b5537c3SGreg Roach                    $tmp = implode('-', $exclude);
55222d65e5aSGreg Roach                    if (in_array($tmp, $excluded, true)) {
5539b5537c3SGreg Roach                        continue;
5549b5537c3SGreg Roach                    }
5559b5537c3SGreg Roach
5569b5537c3SGreg Roach                    $excluded[] = $tmp;
5579b5537c3SGreg Roach                    // Add any new path to the queue
5589b5537c3SGreg Roach                    foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) {
5599b5537c3SGreg Roach                        $queue[] = [
5609b5537c3SGreg Roach                            'path'    => $new_path,
5619b5537c3SGreg Roach                            'exclude' => $exclude,
5629b5537c3SGreg Roach                        ];
5639b5537c3SGreg Roach                    }
5649b5537c3SGreg Roach                }
5659b5537c3SGreg Roach            }
5669b5537c3SGreg Roach        }
567185cbb4dSGreg Roach        // Extract the paths from the queue.
5689b5537c3SGreg Roach        $paths = [];
5699b5537c3SGreg Roach        foreach ($queue as $next) {
570185cbb4dSGreg Roach            // The Dijkstra library does not use strict types, and converts
571185cbb4dSGreg Roach            // numeric array keys (XREFs) from strings to integers;
572185cbb4dSGreg Roach            $path = array_map($this->stringMapper(), $next['path']);
573185cbb4dSGreg Roach
574185cbb4dSGreg Roach            // Remove duplicates
575185cbb4dSGreg Roach            $paths[implode('-', $next['path'])] = $path;
5769b5537c3SGreg Roach        }
5779b5537c3SGreg Roach
5789b5537c3SGreg Roach        return $paths;
5799b5537c3SGreg Roach    }
5809b5537c3SGreg Roach
5819b5537c3SGreg Roach    /**
582185cbb4dSGreg Roach     * Convert numeric values to strings
583185cbb4dSGreg Roach     *
584c6921a17SGreg Roach     * @return Closure(int|string):string
585185cbb4dSGreg Roach     */
586c3f3b628SGreg Roach    private function stringMapper(): Closure
587c3f3b628SGreg Roach    {
588f25fc0f9SGreg Roach        return static fn($xref) => (string) $xref;
589185cbb4dSGreg Roach    }
590185cbb4dSGreg Roach
591185cbb4dSGreg Roach    /**
5929b5537c3SGreg Roach     * Find all ancestors of a list of individuals
5939b5537c3SGreg Roach     *
5949b5537c3SGreg Roach     * @param string $xref1
5959b5537c3SGreg Roach     * @param string $xref2
5969b5537c3SGreg Roach     * @param int    $tree_id
5979b5537c3SGreg Roach     *
59824f2a3afSGreg Roach     * @return array<string>
5999b5537c3SGreg Roach     */
60024f2a3afSGreg Roach    private function allAncestors(string $xref1, string $xref2, int $tree_id): array
6019b5537c3SGreg Roach    {
6029b5537c3SGreg Roach        $ancestors = [
6039b5537c3SGreg Roach            $xref1,
6049b5537c3SGreg Roach            $xref2,
6059b5537c3SGreg Roach        ];
6069b5537c3SGreg Roach
6079b5537c3SGreg Roach        $queue = [
6089b5537c3SGreg Roach            $xref1,
6099b5537c3SGreg Roach            $xref2,
6109b5537c3SGreg Roach        ];
611075d1a05SGreg Roach        while ($queue !== []) {
6129b5537c3SGreg Roach            $parents = DB::table('link AS l1')
6130b5fd0a6SGreg Roach                ->join('link AS l2', static function (JoinClause $join): void {
6149b5537c3SGreg Roach                    $join
6159b5537c3SGreg Roach                        ->on('l1.l_to', '=', 'l2.l_to')
6169b5537c3SGreg Roach                        ->on('l1.l_file', '=', 'l2.l_file');
6179b5537c3SGreg Roach                })
6189b5537c3SGreg Roach                ->where('l1.l_file', '=', $tree_id)
6199b5537c3SGreg Roach                ->where('l1.l_type', '=', 'FAMC')
6209b5537c3SGreg Roach                ->where('l2.l_type', '=', 'FAMS')
6219b5537c3SGreg Roach                ->whereIn('l1.l_from', $queue)
6229b5537c3SGreg Roach                ->pluck('l2.l_from');
6239b5537c3SGreg Roach
6249b5537c3SGreg Roach            $queue = [];
6259b5537c3SGreg Roach            foreach ($parents as $parent) {
62622d65e5aSGreg Roach                if (!in_array($parent, $ancestors, true)) {
6279b5537c3SGreg Roach                    $ancestors[] = $parent;
6289b5537c3SGreg Roach                    $queue[]     = $parent;
6299b5537c3SGreg Roach                }
6309b5537c3SGreg Roach            }
6319b5537c3SGreg Roach        }
6329b5537c3SGreg Roach
6339b5537c3SGreg Roach        return $ancestors;
6349b5537c3SGreg Roach    }
6359b5537c3SGreg Roach
6369b5537c3SGreg Roach    /**
6379b5537c3SGreg Roach     * Find all families of two individuals
6389b5537c3SGreg Roach     *
6399b5537c3SGreg Roach     * @param string $xref1
6409b5537c3SGreg Roach     * @param string $xref2
6419b5537c3SGreg Roach     * @param int    $tree_id
6429b5537c3SGreg Roach     *
64324f2a3afSGreg Roach     * @return array<string>
6449b5537c3SGreg Roach     */
64524f2a3afSGreg Roach    private function excludeFamilies(string $xref1, string $xref2, int $tree_id): array
6469b5537c3SGreg Roach    {
6479b5537c3SGreg Roach        return DB::table('link AS l1')
6480b5fd0a6SGreg Roach            ->join('link AS l2', static function (JoinClause $join): void {
6499b5537c3SGreg Roach                $join
6509b5537c3SGreg Roach                    ->on('l1.l_to', '=', 'l2.l_to')
6519b5537c3SGreg Roach                    ->on('l1.l_type', '=', 'l2.l_type')
6529b5537c3SGreg Roach                    ->on('l1.l_file', '=', 'l2.l_file');
6539b5537c3SGreg Roach            })
6549b5537c3SGreg Roach            ->where('l1.l_file', '=', $tree_id)
6559b5537c3SGreg Roach            ->where('l1.l_type', '=', 'FAMS')
6569b5537c3SGreg Roach            ->where('l1.l_from', '=', $xref1)
6579b5537c3SGreg Roach            ->where('l2.l_from', '=', $xref2)
6589b5537c3SGreg Roach            ->pluck('l1.l_to')
6599b5537c3SGreg Roach            ->all();
6609b5537c3SGreg Roach    }
6619b5537c3SGreg Roach
6629b5537c3SGreg Roach    /**
6636ccdf4f0SGreg Roach     * Convert a path (list of XREFs) to an "old-style" string of relationships.
6646ccdf4f0SGreg Roach     * Return an empty array, if privacy rules prevent us viewing any node.
6656ccdf4f0SGreg Roach     *
6666ccdf4f0SGreg Roach     * @param Tree          $tree
66709482a55SGreg Roach     * @param array<string> $path Alternately Individual / Family
6686ccdf4f0SGreg Roach     *
66924f2a3afSGreg Roach     * @return array<string>
6706ccdf4f0SGreg Roach     */
6716ccdf4f0SGreg Roach    private function oldStyleRelationshipPath(Tree $tree, array $path): array
6726ccdf4f0SGreg Roach    {
6736ccdf4f0SGreg Roach        $spouse_codes = [
6746ccdf4f0SGreg Roach            'M' => 'hus',
6756ccdf4f0SGreg Roach            'F' => 'wif',
6766ccdf4f0SGreg Roach            'U' => 'spo',
6776ccdf4f0SGreg Roach        ];
6786ccdf4f0SGreg Roach        $parent_codes = [
6796ccdf4f0SGreg Roach            'M' => 'fat',
6806ccdf4f0SGreg Roach            'F' => 'mot',
6816ccdf4f0SGreg Roach            'U' => 'par',
6826ccdf4f0SGreg Roach        ];
6836ccdf4f0SGreg Roach        $child_codes = [
6846ccdf4f0SGreg Roach            'M' => 'son',
6856ccdf4f0SGreg Roach            'F' => 'dau',
6866ccdf4f0SGreg Roach            'U' => 'chi',
6876ccdf4f0SGreg Roach        ];
6886ccdf4f0SGreg Roach        $sibling_codes = [
6896ccdf4f0SGreg Roach            'M' => 'bro',
6906ccdf4f0SGreg Roach            'F' => 'sis',
6916ccdf4f0SGreg Roach            'U' => 'sib',
6926ccdf4f0SGreg Roach        ];
6936ccdf4f0SGreg Roach        $relationships = [];
6946ccdf4f0SGreg Roach
6956ccdf4f0SGreg Roach        for ($i = 1, $count = count($path); $i < $count; $i += 2) {
6966b9cb339SGreg Roach            $family = Registry::familyFactory()->make($path[$i], $tree);
6976b9cb339SGreg Roach            $prev   = Registry::individualFactory()->make($path[$i - 1], $tree);
6986b9cb339SGreg Roach            $next   = Registry::individualFactory()->make($path[$i + 1], $tree);
6996ccdf4f0SGreg Roach            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) {
7006ccdf4f0SGreg Roach                $rel1 = $match[1];
7016ccdf4f0SGreg Roach            } else {
7026ccdf4f0SGreg Roach                return [];
7036ccdf4f0SGreg Roach            }
7046ccdf4f0SGreg Roach            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) {
7056ccdf4f0SGreg Roach                $rel2 = $match[1];
7066ccdf4f0SGreg Roach            } else {
7076ccdf4f0SGreg Roach                return [];
7086ccdf4f0SGreg Roach            }
7096ccdf4f0SGreg Roach            if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
71023a98013SGreg Roach                $relationships[$i] = $spouse_codes[$next->sex()] ?? $spouse_codes['U'];
7116ccdf4f0SGreg Roach            } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') {
71223a98013SGreg Roach                $relationships[$i] = $child_codes[$next->sex()] ?? $child_codes['U'];
7136ccdf4f0SGreg Roach            } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
71423a98013SGreg Roach                $relationships[$i] = $parent_codes[$next->sex()] ?? $parent_codes['U'];
7156ccdf4f0SGreg Roach            } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') {
71623a98013SGreg Roach                $relationships[$i] = $sibling_codes[$next->sex()] ?? $sibling_codes['U'];
7176ccdf4f0SGreg Roach            }
7186ccdf4f0SGreg Roach        }
7196ccdf4f0SGreg Roach
7206ccdf4f0SGreg Roach        return $relationships;
7216ccdf4f0SGreg Roach    }
7226ccdf4f0SGreg Roach
7236ccdf4f0SGreg Roach    /**
7249b5537c3SGreg Roach     * Possible options for the recursion option
7259b5537c3SGreg Roach     *
7269b5537c3SGreg Roach     * @param int $max_recursion
7279b5537c3SGreg Roach     *
72824f2a3afSGreg Roach     * @return array<string>
7299b5537c3SGreg Roach     */
7309b5537c3SGreg Roach    private function recursionOptions(int $max_recursion): array
7319b5537c3SGreg Roach    {
7323dcc812bSGreg Roach        if ($max_recursion === static::UNLIMITED_RECURSION) {
7339b5537c3SGreg Roach            $text = I18N::translate('Find all possible relationships');
7349b5537c3SGreg Roach        } else {
7359b5537c3SGreg Roach            $text = I18N::translate('Find other relationships');
7369b5537c3SGreg Roach        }
7379b5537c3SGreg Roach
7389b5537c3SGreg Roach        return [
7399b5537c3SGreg Roach            '0'            => I18N::translate('Find the closest relationships'),
7409b5537c3SGreg Roach            $max_recursion => $text,
7419b5537c3SGreg Roach        ];
7429b5537c3SGreg Roach    }
743168ff6f3Sric2016}
744