Passed
Push — master ( c81b7b...3cfcc8 )
by Greg
06:29
created

RelationshipsChartModule::chartMenu()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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