Passed
Push — master ( b9de05...5e23c3 )
by Greg
06:49
created

RelationshipsChartModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Aura\Router\RouterContainer;
23
use Closure;
24
use Fig\Http\Message\RequestMethodInterface;
25
use Fisharebest\Algorithm\Dijkstra;
26
use Fisharebest\Webtrees\Auth;
27
use Fisharebest\Webtrees\Family;
28
use Fisharebest\Webtrees\FlashMessages;
29
use Fisharebest\Webtrees\Functions\Functions;
30
use Fisharebest\Webtrees\I18N;
31
use Fisharebest\Webtrees\Individual;
32
use Fisharebest\Webtrees\Menu;
33
use Fisharebest\Webtrees\Services\TreeService;
34
use Fisharebest\Webtrees\Tree;
35
use Illuminate\Database\Capsule\Manager as DB;
36
use Illuminate\Database\Query\JoinClause;
37
use Psr\Http\Message\ResponseInterface;
38
use Psr\Http\Message\ServerRequestInterface;
39
use Psr\Http\Server\RequestHandlerInterface;
40
41
use function redirect;
42
use function route;
43
use function view;
44
45
/**
46
 * Class RelationshipsChartModule
47
 */
48
class RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
49
{
50
    use ModuleChartTrait;
51
    use ModuleConfigTrait;
52
53
    private const ROUTE_NAME = 'relationships';
54
    private const ROUTE_URL  = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}';
55
56
    /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */
57
    public const UNLIMITED_RECURSION = 99;
58
59
    /** By default new trees allow unlimited recursion */
60
    public const DEFAULT_RECURSION = '99';
61
62
    /** By default new trees search for all relationships (not via ancestors) */
63
    public const DEFAULT_ANCESTORS  = '0';
64
    public const DEFAULT_PARAMETERS = [
65
        'ancestors' => self::DEFAULT_ANCESTORS,
66
        'recursion' => self::DEFAULT_RECURSION,
67
    ];
68
69
    /** @var TreeService */
70
    private $tree_service;
71
72
    /**
73
     * ModuleController constructor.
74
     *
75
     * @param TreeService   $tree_service
76
     */
77
    public function __construct(TreeService $tree_service)
78
    {
79
        $this->tree_service = $tree_service;
80
    }
81
82
    /**
83
     * Initialization.
84
     *
85
     * @param RouterContainer $router_container
86
     */
87
    public function boot(RouterContainer $router_container)
88
    {
89
        $router_container->getMap()
90
            ->get(self::ROUTE_NAME, self::ROUTE_URL, self::class)
91
            ->allows(RequestMethodInterface::METHOD_POST)
92
            ->tokens([
93
                'ancestors' => '\d+',
94
                'recursion' => '\d+',
95
            ])->defaults([
96
                'xref2' => '',
97
            ]);
98
    }
99
100
    /**
101
     * A sentence describing what this module does.
102
     *
103
     * @return string
104
     */
105
    public function description(): string
106
    {
107
        /* I18N: Description of the “RelationshipsChart” module */
108
        return I18N::translate('A chart displaying relationships between two individuals.');
109
    }
110
111
    /**
112
     * Return a menu item for this chart - for use in individual boxes.
113
     *
114
     * @param Individual $individual
115
     *
116
     * @return Menu|null
117
     */
118
    public function chartBoxMenu(Individual $individual): ?Menu
119
    {
120
        return $this->chartMenu($individual);
121
    }
122
123
    /**
124
     * A main menu item for this chart.
125
     *
126
     * @param Individual $individual
127
     *
128
     * @return Menu
129
     */
130
    public function chartMenu(Individual $individual): Menu
131
    {
132
        $gedcomid = $individual->tree()->getUserPreference(Auth::user(), 'gedcomid');
133
134
        if ($gedcomid !== '' && $gedcomid !== $individual->xref()) {
135
            return new Menu(
136
                I18N::translate('Relationship to me'),
137
                $this->chartUrl($individual, ['xref2' => $gedcomid]),
138
                $this->chartMenuClass(),
139
                $this->chartUrlAttributes()
140
            );
141
        }
142
143
        return new Menu(
144
            $this->title(),
145
            $this->chartUrl($individual),
146
            $this->chartMenuClass(),
147
            $this->chartUrlAttributes()
148
        );
149
    }
150
151
    /**
152
     * CSS class for the URL.
153
     *
154
     * @return string
155
     */
156
    public function chartMenuClass(): string
157
    {
158
        return 'menu-chart-relationship';
159
    }
160
161
    /**
162
     * How should this module be identified in the control panel, etc.?
163
     *
164
     * @return string
165
     */
166
    public function title(): string
167
    {
168
        /* I18N: Name of a module/chart */
169
        return I18N::translate('Relationships');
170
    }
171
172
    /**
173
     * The URL for a page showing chart options.
174
     *
175
     * @param Individual $individual
176
     * @param mixed[]    $parameters
177
     *
178
     * @return string
179
     */
180
    public function chartUrl(Individual $individual, array $parameters = []): string
181
    {
182
        return route(self::ROUTE_NAME, [
183
                'xref' => $individual->xref(),
184
                'tree' => $individual->tree()->name(),
185
            ] + $parameters + self::DEFAULT_PARAMETERS);
186
    }
187
188
    /**
189
     * @param ServerRequestInterface $request
190
     *
191
     * @return ResponseInterface
192
     */
193
    public function handle(ServerRequestInterface $request): ResponseInterface
194
    {
195
        $ajax      = $request->getQueryParams()['ajax'] ?? '';
196
        $ancestors = (int) $request->getAttribute('ancestors');
197
        $recursion = (int) $request->getAttribute('recursion');
198
        $tree      = $request->getAttribute('tree');
199
        $user      = $request->getAttribute('user');
200
        $xref      = $request->getAttribute('xref');
201
        $xref2     = $request->getAttribute('xref2');
202
203
        // Convert POST requests into GET requests for pretty URLs.
204
        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
205
            return redirect(route(self::ROUTE_NAME, [
206
                'ancestors' => $request->getParsedBody()['ancestors'],
207
                'recursion' => $request->getParsedBody()['recursion'],
208
                'tree'      => $request->getAttribute('tree')->name(),
209
                'xref'      => $request->getParsedBody()['xref'],
210
                'xref2'     => $request->getParsedBody()['xref2'],
211
            ]));
212
        }
213
214
        $individual1 = Individual::getInstance($xref, $tree);
215
        $individual2 = Individual::getInstance($xref2, $tree);
216
217
        $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS);
218
        $max_recursion  = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
219
220
        $recursion = min($recursion, $max_recursion);
221
222
        if ($tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') !== '1') {
223
            if ($individual1 instanceof Individual) {
224
                Auth::checkIndividualAccess($individual1);
225
            }
226
227
            if ($individual2 instanceof Individual) {
228
                Auth::checkIndividualAccess($individual2);
229
            }
230
        }
231
232
        Auth::checkComponentAccess($this, 'chart', $tree, $user);
233
234
        if ($individual1 instanceof Individual && $individual2 instanceof Individual) {
235
            if ($ajax === '1') {
236
                return $this->chart($individual1, $individual2, $recursion, $ancestors);
237
            }
238
239
            /* I18N: %s are individual’s names */
240
            $title    = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName());
241
            $ajax_url = $this->chartUrl($individual1, [
242
                'ajax'      => true,
243
                'ancestors' => $ancestors,
244
                'recursion' => $recursion,
245
                'xref2'     => $individual2->xref(),
246
            ]);
247
        } else {
248
            $title    = I18N::translate('Relationships');
249
            $ajax_url = '';
250
        }
251
252
        return $this->viewResponse('modules/relationships-chart/page', [
253
            'ajax_url'          => $ajax_url,
254
            'ancestors'         => $ancestors,
255
            'ancestors_only'    => $ancestors_only,
256
            'ancestors_options' => $this->ancestorsOptions(),
257
            'individual1'       => $individual1,
258
            'individual2'       => $individual2,
259
            'max_recursion'     => $max_recursion,
260
            'module'            => $this->name(),
261
            'recursion'         => $recursion,
262
            'recursion_options' => $this->recursionOptions($max_recursion),
263
            'title'             => $title,
264
            'tree'              => $tree,
265
        ]);
266
    }
267
268
    /**
269
     * @param Individual $individual1
270
     * @param Individual $individual2
271
     * @param int        $recursion
272
     * @param int        $ancestors
273
     *
274
     * @return ResponseInterface
275
     */
276
    public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface
277
    {
278
        $tree = $individual1->tree();
279
280
        $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
281
282
        $recursion = min($recursion, $max_recursion);
283
284
        $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors);
285
286
        // @TODO - convert to views
287
        ob_start();
288
        if (I18N::direction() === 'ltr') {
289
            $diagonal1 = asset('css/images/dline.png');
290
            $diagonal2 = asset('css/images/dline2.png');
291
        } else {
292
            $diagonal1 = asset('css/images/dline2.png');
293
            $diagonal2 = asset('css/images/dline.png');
294
        }
295
296
        $num_paths = 0;
297
        foreach ($paths as $path) {
298
            // Extract the relationship names between pairs of individuals
299
            $relationships = $this->oldStyleRelationshipPath($tree, $path);
300
            if (empty($relationships)) {
301
                // Cannot see one of the families/individuals, due to privacy;
302
                continue;
303
            }
304
            echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>';
305
            $num_paths++;
306
307
            // Use a table/grid for layout.
308
            $table = [];
309
            // Current position in the grid.
310
            $x = 0;
311
            $y = 0;
312
            // Extent of the grid.
313
            $min_y = 0;
314
            $max_y = 0;
315
            $max_x = 0;
316
            // For each node in the path.
317
            foreach ($path as $n => $xref) {
318
                if ($n % 2 === 1) {
319
                    switch ($relationships[$n]) {
320
                        case 'hus':
321
                        case 'wif':
322
                        case 'spo':
323
                        case 'bro':
324
                        case 'sis':
325
                        case 'sib':
326
                            $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>';
327
                            $x                 += 2;
328
                            break;
329
                        case 'son':
330
                        case 'dau':
331
                        case 'chi':
332
                            if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) {
333
                                $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>';
334
                                $x                     += 2;
335
                            } else {
336
                                $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>';
337
                            }
338
                            $y -= 2;
339
                            break;
340
                        case 'fat':
341
                        case 'mot':
342
                        case 'par':
343
                            if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) {
344
                                $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>';
345
                                $x                     += 2;
346
                            } else {
347
                                $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>';
348
                            }
349
                            $y += 2;
350
                            break;
351
                    }
352
                    $max_x = max($max_x, $x);
353
                    $min_y = min($min_y, $y);
354
                    $max_y = max($max_y, $y);
355
                } else {
356
                    $individual    = Individual::getInstance($xref, $tree);
357
                    $table[$x][$y] = view('chart-box', ['individual' => $individual]);
358
                }
359
            }
360
            echo '<div class="wt-chart wt-chart-relationships">';
361
            echo '<table style="border-collapse: collapse; margin: 20px 50px;">';
362
            for ($y = $max_y; $y >= $min_y; --$y) {
363
                echo '<tr>';
364
                for ($x = 0; $x <= $max_x; ++$x) {
365
                    echo '<td style="padding: 0;">';
366
                    if (isset($table[$x][$y])) {
367
                        echo $table[$x][$y];
368
                    }
369
                    echo '</td>';
370
                }
371
                echo '</tr>';
372
            }
373
            echo '</table>';
374
            echo '</div>';
375
        }
376
377
        if (!$num_paths) {
378
            echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>';
379
        }
380
381
        $html = ob_get_clean();
382
383
        return response($html);
384
    }
385
386
    /**
387
     * @param ServerRequestInterface $request
388
     *
389
     * @return ResponseInterface
390
     */
391
    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

391
    public function getAdminAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
392
    {
393
        $this->layout = 'layouts/administration';
394
395
        return $this->viewResponse('modules/relationships-chart/config', [
396
            'all_trees'         => $this->tree_service->all(),
397
            'ancestors_options' => $this->ancestorsOptions(),
398
            'default_ancestors' => self::DEFAULT_ANCESTORS,
399
            'default_recursion' => self::DEFAULT_RECURSION,
400
            'recursion_options' => $this->recursionConfigOptions(),
401
            'title'             => I18N::translate('Chart preferences') . ' — ' . $this->title(),
402
        ]);
403
    }
404
405
    /**
406
     * @param ServerRequestInterface $request
407
     *
408
     * @return ResponseInterface
409
     */
410
    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
411
    {
412
        foreach ($this->tree_service->all() as $tree) {
413
            $recursion = $request->getParsedBody()['relationship-recursion-' . $tree->id()] ?? '';
414
            $ancestors = $request->getParsedBody()['relationship-ancestors-' . $tree->id()] ?? '';
415
416
            $tree->setPreference('RELATIONSHIP_RECURSION', $recursion);
417
            $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors);
418
        }
419
420
        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
421
422
        return redirect($this->getConfigLink());
423
    }
424
425
    /**
426
     * Possible options for the ancestors option
427
     *
428
     * @return string[]
429
     */
430
    private function ancestorsOptions(): array
431
    {
432
        return [
433
            0 => I18N::translate('Find any relationship'),
434
            1 => I18N::translate('Find relationships via ancestors'),
435
        ];
436
    }
437
438
    /**
439
     * Possible options for the recursion option
440
     *
441
     * @return string[]
442
     */
443
    private function recursionConfigOptions(): array
444
    {
445
        return [
446
            0                         => I18N::translate('none'),
447
            1                         => I18N::number(1),
448
            2                         => I18N::number(2),
449
            3                         => I18N::number(3),
450
            self::UNLIMITED_RECURSION => I18N::translate('unlimited'),
451
        ];
452
    }
453
454
    /**
455
     * Calculate the shortest paths - or all paths - between two individuals.
456
     *
457
     * @param Individual $individual1
458
     * @param Individual $individual2
459
     * @param int        $recursion How many levels of recursion to use
460
     * @param bool       $ancestor  Restrict to relationships via a common ancestor
461
     *
462
     * @return string[][]
463
     */
464
    private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array
465
    {
466
        $tree = $individual1->tree();
467
468
        $rows = DB::table('link')
469
            ->where('l_file', '=', $tree->id())
470
            ->whereIn('l_type', ['FAMS', 'FAMC'])
471
            ->select(['l_from', 'l_to'])
472
            ->get();
473
474
        // Optionally restrict the graph to the ancestors of the individuals.
475
        if ($ancestor) {
476
            $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id());
477
            $exclude   = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id());
478
        } else {
479
            $ancestors = [];
480
            $exclude   = [];
481
        }
482
483
        $graph = [];
484
485
        foreach ($rows as $row) {
486
            if (empty($ancestors) || in_array($row->l_from, $ancestors, true) && !in_array($row->l_to, $exclude, true)) {
487
                $graph[$row->l_from][$row->l_to] = 1;
488
                $graph[$row->l_to][$row->l_from] = 1;
489
            }
490
        }
491
492
        $xref1    = $individual1->xref();
493
        $xref2    = $individual2->xref();
494
        $dijkstra = new Dijkstra($graph);
495
        $paths    = $dijkstra->shortestPaths($xref1, $xref2);
496
497
        // Only process each exclusion list once;
498
        $excluded = [];
499
500
        $queue = [];
501
        foreach ($paths as $path) {
502
            // Insert the paths into the queue, with an exclusion list.
503
            $queue[] = [
504
                'path'    => $path,
505
                'exclude' => [],
506
            ];
507
            // While there are un-extended paths
508
            for ($next = current($queue); $next !== false; $next = next($queue)) {
509
                // For each family on the path
510
                for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) {
511
                    $exclude = $next['exclude'];
512
                    if (count($exclude) >= $recursion) {
513
                        continue;
514
                    }
515
                    $exclude[] = $next['path'][$n];
516
                    sort($exclude);
517
                    $tmp = implode('-', $exclude);
518
                    if (in_array($tmp, $excluded, true)) {
519
                        continue;
520
                    }
521
522
                    $excluded[] = $tmp;
523
                    // Add any new path to the queue
524
                    foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) {
525
                        $queue[] = [
526
                            'path'    => $new_path,
527
                            'exclude' => $exclude,
528
                        ];
529
                    }
530
                }
531
            }
532
        }
533
        // Extract the paths from the queue.
534
        $paths = [];
535
        foreach ($queue as $next) {
536
            // The Dijkstra library does not use strict types, and converts
537
            // numeric array keys (XREFs) from strings to integers;
538
            $path = array_map($this->stringMapper(), $next['path']);
539
540
            // Remove duplicates
541
            $paths[implode('-', $next['path'])] = $path;
542
        }
543
544
        return $paths;
545
    }
546
547
    /**
548
     * Convert numeric values to strings
549
     *
550
     * @return Closure
551
     */
552
    private function stringMapper(): Closure
553
    {
554
        return static function ($xref) {
555
            return (string) $xref;
556
        };
557
    }
558
559
    /**
560
     * Find all ancestors of a list of individuals
561
     *
562
     * @param string $xref1
563
     * @param string $xref2
564
     * @param int    $tree_id
565
     *
566
     * @return string[]
567
     */
568
    private function allAncestors($xref1, $xref2, $tree_id): array
569
    {
570
        $ancestors = [
571
            $xref1,
572
            $xref2,
573
        ];
574
575
        $queue = [
576
            $xref1,
577
            $xref2,
578
        ];
579
        while (!empty($queue)) {
580
            $parents = DB::table('link AS l1')
581
                ->join('link AS l2', static function (JoinClause $join): void {
582
                    $join
583
                        ->on('l1.l_to', '=', 'l2.l_to')
584
                        ->on('l1.l_file', '=', 'l2.l_file');
585
                })
586
                ->where('l1.l_file', '=', $tree_id)
587
                ->where('l1.l_type', '=', 'FAMC')
588
                ->where('l2.l_type', '=', 'FAMS')
589
                ->whereIn('l1.l_from', $queue)
590
                ->pluck('l2.l_from');
591
592
            $queue = [];
593
            foreach ($parents as $parent) {
594
                if (!in_array($parent, $ancestors, true)) {
595
                    $ancestors[] = $parent;
596
                    $queue[]     = $parent;
597
                }
598
            }
599
        }
600
601
        return $ancestors;
602
    }
603
604
    /**
605
     * Find all families of two individuals
606
     *
607
     * @param string $xref1
608
     * @param string $xref2
609
     * @param int    $tree_id
610
     *
611
     * @return string[]
612
     */
613
    private function excludeFamilies($xref1, $xref2, $tree_id): array
614
    {
615
        return DB::table('link AS l1')
616
            ->join('link AS l2', static function (JoinClause $join): void {
617
                $join
618
                    ->on('l1.l_to', '=', 'l2.l_to')
619
                    ->on('l1.l_type', '=', 'l2.l_type')
620
                    ->on('l1.l_file', '=', 'l2.l_file');
621
            })
622
            ->where('l1.l_file', '=', $tree_id)
623
            ->where('l1.l_type', '=', 'FAMS')
624
            ->where('l1.l_from', '=', $xref1)
625
            ->where('l2.l_from', '=', $xref2)
626
            ->pluck('l1.l_to')
627
            ->all();
628
    }
629
630
    /**
631
     * Convert a path (list of XREFs) to an "old-style" string of relationships.
632
     * Return an empty array, if privacy rules prevent us viewing any node.
633
     *
634
     * @param Tree     $tree
635
     * @param string[] $path Alternately Individual / Family
636
     *
637
     * @return string[]
638
     */
639
    private function oldStyleRelationshipPath(Tree $tree, array $path): array
640
    {
641
        $spouse_codes  = [
642
            'M' => 'hus',
643
            'F' => 'wif',
644
            'U' => 'spo',
645
        ];
646
        $parent_codes  = [
647
            'M' => 'fat',
648
            'F' => 'mot',
649
            'U' => 'par',
650
        ];
651
        $child_codes   = [
652
            'M' => 'son',
653
            'F' => 'dau',
654
            'U' => 'chi',
655
        ];
656
        $sibling_codes = [
657
            'M' => 'bro',
658
            'F' => 'sis',
659
            'U' => 'sib',
660
        ];
661
        $relationships = [];
662
663
        for ($i = 1, $count = count($path); $i < $count; $i += 2) {
664
            $family = Family::getInstance($path[$i], $tree);
665
            $prev   = Individual::getInstance($path[$i - 1], $tree);
666
            $next   = Individual::getInstance($path[$i + 1], $tree);
667
            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) {
668
                $rel1 = $match[1];
669
            } else {
670
                return [];
671
            }
672
            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) {
673
                $rel2 = $match[1];
674
            } else {
675
                return [];
676
            }
677
            if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
678
                $relationships[$i] = $spouse_codes[$next->sex()];
679
            } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') {
680
                $relationships[$i] = $child_codes[$next->sex()];
681
            } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
682
                $relationships[$i] = $parent_codes[$next->sex()];
683
            } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') {
684
                $relationships[$i] = $sibling_codes[$next->sex()];
685
            }
686
        }
687
688
        return $relationships;
689
    }
690
691
    /**
692
     * Possible options for the recursion option
693
     *
694
     * @param int $max_recursion
695
     *
696
     * @return string[]
697
     */
698
    private function recursionOptions(int $max_recursion): array
699
    {
700
        if ($max_recursion === static::UNLIMITED_RECURSION) {
701
            $text = I18N::translate('Find all possible relationships');
702
        } else {
703
            $text = I18N::translate('Find other relationships');
704
        }
705
706
        return [
707
            '0'            => I18N::translate('Find the closest relationships'),
708
            $max_recursion => $text,
709
        ];
710
    }
711
}
712