Passed
Push — master ( d2d9e8...185cbb )
by Greg
06:00
created

RelationshipsChartModule::stringMapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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