Issues (2558)

app/Module/RelationshipsChartModule.php (1 issue)

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