xref: /webtrees/app/Module/BranchesListModule.php (revision 0aed7de408dd15088bb898201ea57733c6d925ee)
167992b6aSRichard Cissee<?php
23976b470SGreg Roach
367992b6aSRichard Cissee/**
467992b6aSRichard Cissee * webtrees: online genealogy
55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team
667992b6aSRichard Cissee * This program is free software: you can redistribute it and/or modify
767992b6aSRichard Cissee * it under the terms of the GNU General Public License as published by
867992b6aSRichard Cissee * the Free Software Foundation, either version 3 of the License, or
967992b6aSRichard Cissee * (at your option) any later version.
1067992b6aSRichard Cissee * This program is distributed in the hope that it will be useful,
1167992b6aSRichard Cissee * but WITHOUT ANY WARRANTY; without even the implied warranty of
1267992b6aSRichard Cissee * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1367992b6aSRichard Cissee * GNU General Public License for more details.
1467992b6aSRichard Cissee * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1667992b6aSRichard Cissee */
17fcfa147eSGreg Roach
1867992b6aSRichard Cisseedeclare(strict_types=1);
1967992b6aSRichard Cissee
2067992b6aSRichard Cisseenamespace Fisharebest\Webtrees\Module;
2167992b6aSRichard Cissee
2206a438b4SGreg Roachuse Fig\Http\Message\RequestMethodInterface;
23808ea2d4SGreg Roachuse Fisharebest\Webtrees\Auth;
2406a438b4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
257c29ac65SGreg Roachuse Fisharebest\Webtrees\Elements\PedigreeLinkageType;
2606a438b4SGreg Roachuse Fisharebest\Webtrees\Family;
2706a438b4SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2867992b6aSRichard Cisseeuse Fisharebest\Webtrees\I18N;
2906a438b4SGreg Roachuse Fisharebest\Webtrees\Individual;
3009482a55SGreg Roachuse Fisharebest\Webtrees\Registry;
3167992b6aSRichard Cisseeuse Fisharebest\Webtrees\Services\ModuleService;
3206a438b4SGreg Roachuse Fisharebest\Webtrees\Soundex;
3367992b6aSRichard Cisseeuse Fisharebest\Webtrees\Tree;
34b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
3506a438b4SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
3606a438b4SGreg Roachuse Illuminate\Database\Query\Builder;
3706a438b4SGreg Roachuse Illuminate\Database\Query\JoinClause;
386ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
396ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
4006a438b4SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
41f3874e19SGreg Roach
4206a438b4SGreg Roachuse function app;
4306a438b4SGreg Roachuse function array_search;
445229eadeSGreg Roachuse function assert;
4506a438b4SGreg Roachuse function e;
4606a438b4SGreg Roachuse function explode;
4706a438b4SGreg Roachuse function in_array;
4806a438b4SGreg Roachuse function is_int;
4906a438b4SGreg Roachuse function key;
5006a438b4SGreg Roachuse function log;
5106a438b4SGreg Roachuse function next;
52808ea2d4SGreg Roachuse function redirect;
53808ea2d4SGreg Roachuse function route;
5406a438b4SGreg Roachuse function strip_tags;
5506a438b4SGreg Roachuse function stripos;
56382922aaSGreg Roachuse function strtolower;
5706a438b4SGreg Roachuse function usort;
5806a438b4SGreg Roachuse function view;
5967992b6aSRichard Cissee
6067992b6aSRichard Cissee/**
6167992b6aSRichard Cissee * Class BranchesListModule
6267992b6aSRichard Cissee */
6306a438b4SGreg Roachclass BranchesListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
6467992b6aSRichard Cissee{
6567992b6aSRichard Cissee    use ModuleListTrait;
6667992b6aSRichard Cissee
6706a438b4SGreg Roach    protected const ROUTE_URL = '/tree/{tree}/branches{/surname}';
6806a438b4SGreg Roach
6943f2f523SGreg Roach    private ModuleService $module_service;
7006a438b4SGreg Roach
7106a438b4SGreg Roach    /**
7206a438b4SGreg Roach     * BranchesListModule constructor.
7306a438b4SGreg Roach     *
7406a438b4SGreg Roach     * @param ModuleService $module_service
7506a438b4SGreg Roach     */
7606a438b4SGreg Roach    public function __construct(ModuleService $module_service)
7706a438b4SGreg Roach    {
7806a438b4SGreg Roach        $this->module_service = $module_service;
7906a438b4SGreg Roach    }
8006a438b4SGreg Roach
8106a438b4SGreg Roach    /**
8206a438b4SGreg Roach     * Initialization.
8306a438b4SGreg Roach     *
8406a438b4SGreg Roach     * @return void
8506a438b4SGreg Roach     */
8606a438b4SGreg Roach    public function boot(): void
8706a438b4SGreg Roach    {
88158900c2SGreg Roach        Registry::routeFactory()->routeMap()
8906a438b4SGreg Roach            ->get(static::class, static::ROUTE_URL, $this)
9006a438b4SGreg Roach            ->allows(RequestMethodInterface::METHOD_POST);
9106a438b4SGreg Roach    }
9206a438b4SGreg Roach
9367992b6aSRichard Cissee    /**
940cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
9567992b6aSRichard Cissee     *
9667992b6aSRichard Cissee     * @return string
9767992b6aSRichard Cissee     */
9867992b6aSRichard Cissee    public function title(): string
9967992b6aSRichard Cissee    {
10067992b6aSRichard Cissee        /* I18N: Name of a module/list */
10167992b6aSRichard Cissee        return I18N::translate('Branches');
10267992b6aSRichard Cissee    }
10367992b6aSRichard Cissee
10467992b6aSRichard Cissee    /**
10567992b6aSRichard Cissee     * A sentence describing what this module does.
10667992b6aSRichard Cissee     *
10767992b6aSRichard Cissee     * @return string
10867992b6aSRichard Cissee     */
10967992b6aSRichard Cissee    public function description(): string
11067992b6aSRichard Cissee    {
111b5e8e56bSGreg Roach        /* I18N: Description of the “Branches” module */
11267992b6aSRichard Cissee        return I18N::translate('A list of branches of a family.');
11367992b6aSRichard Cissee    }
11467992b6aSRichard Cissee
11567992b6aSRichard Cissee    /**
11667992b6aSRichard Cissee     * CSS class for the URL.
11767992b6aSRichard Cissee     *
11867992b6aSRichard Cissee     * @return string
11967992b6aSRichard Cissee     */
12067992b6aSRichard Cissee    public function listMenuClass(): string
12167992b6aSRichard Cissee    {
12267992b6aSRichard Cissee        return 'menu-branches';
12367992b6aSRichard Cissee    }
12467992b6aSRichard Cissee
1254db4b4a9SGreg Roach    /**
1264db4b4a9SGreg Roach     * @param Tree                                      $tree
12776d39c55SGreg Roach     * @param array<bool|int|string|array<string>|null> $parameters
1284db4b4a9SGreg Roach     *
1294db4b4a9SGreg Roach     * @return string
1304db4b4a9SGreg Roach     */
13167992b6aSRichard Cissee    public function listUrl(Tree $tree, array $parameters = []): string
13267992b6aSRichard Cissee    {
133b55cbc6bSGreg Roach        $request = app(ServerRequestInterface::class);
134b55cbc6bSGreg Roach        assert($request instanceof ServerRequestInterface);
135b55cbc6bSGreg Roach
136b55cbc6bSGreg Roach        $xref = Validator::attributes($request)->isXref()->string('xref', '');
13706a438b4SGreg Roach
13806a438b4SGreg Roach        if ($xref !== '') {
1396b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $tree);
14006a438b4SGreg Roach
14106a438b4SGreg Roach            if ($individual instanceof Individual && $individual->canShow()) {
14206a438b4SGreg Roach                $parameters['surname'] = $parameters['surname'] ?? $individual->getAllNames()[0]['surn'] ?? null;
14306a438b4SGreg Roach            }
14467992b6aSRichard Cissee        }
14567992b6aSRichard Cissee
14606a438b4SGreg Roach        $parameters['tree'] = $tree->name();
1475229eadeSGreg Roach
14806a438b4SGreg Roach        return route(static::class, $parameters);
14967992b6aSRichard Cissee    }
15067992b6aSRichard Cissee
1514db4b4a9SGreg Roach    /**
15224f2a3afSGreg Roach     * @return array<string>
1534db4b4a9SGreg Roach     */
15467992b6aSRichard Cissee    public function listUrlAttributes(): array
15567992b6aSRichard Cissee    {
15667992b6aSRichard Cissee        return [];
15767992b6aSRichard Cissee    }
15806a438b4SGreg Roach
15906a438b4SGreg Roach    /**
16006a438b4SGreg Roach     * Handle URLs generated by older versions of webtrees
16106a438b4SGreg Roach     *
16206a438b4SGreg Roach     * @param ServerRequestInterface $request
16306a438b4SGreg Roach     *
16406a438b4SGreg Roach     * @return ResponseInterface
16506a438b4SGreg Roach     */
16606a438b4SGreg Roach    public function getPageAction(ServerRequestInterface $request): ResponseInterface
16706a438b4SGreg Roach    {
168b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
169748dbe15SGreg Roach        $user = Validator::attributes($request)->user();
170b55cbc6bSGreg Roach
171748dbe15SGreg Roach        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
172748dbe15SGreg Roach
173748dbe15SGreg Roach        return redirect($this->listUrl($tree, [
174748dbe15SGreg Roach            'soundex_dm'  => Validator::queryParams($request)->boolean('soundex_dm'),
175748dbe15SGreg Roach            'soundex_std' => Validator::queryParams($request)->boolean('soundex_std'),
176748dbe15SGreg Roach            'surname'     => 'x' . Validator::queryParams($request)->string('surname'),
177748dbe15SGreg Roach        ]));
17806a438b4SGreg Roach    }
17906a438b4SGreg Roach
18006a438b4SGreg Roach    /**
18106a438b4SGreg Roach     * @param ServerRequestInterface $request
18206a438b4SGreg Roach     *
18306a438b4SGreg Roach     * @return ResponseInterface
18406a438b4SGreg Roach     */
18506a438b4SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
18606a438b4SGreg Roach    {
187b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
188b55cbc6bSGreg Roach        $user = Validator::attributes($request)->user();
18906a438b4SGreg Roach
19006a438b4SGreg Roach        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
19106a438b4SGreg Roach
19206a438b4SGreg Roach        // Convert POST requests into GET requests for pretty URLs.
19306a438b4SGreg Roach        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
194748dbe15SGreg Roach            return redirect($this->listUrl($tree, [
195748dbe15SGreg Roach                'soundex_dm'  => Validator::parsedBody($request)->boolean('soundex_dm', false),
196748dbe15SGreg Roach                'soundex_std' => Validator::parsedBody($request)->boolean('soundex_std', false),
197748dbe15SGreg Roach                'surname'     => Validator::parsedBody($request)->string('surname'),
198748dbe15SGreg Roach            ]));
19906a438b4SGreg Roach        }
20006a438b4SGreg Roach
201748dbe15SGreg Roach        $surname     = Validator::attributes($request)->string('surname', '');
202748dbe15SGreg Roach        $soundex_std = Validator::queryParams($request)->boolean('soundex_std', false);
203748dbe15SGreg Roach        $soundex_dm  = Validator::queryParams($request)->boolean('soundex_dm', false);
204b55cbc6bSGreg Roach        $ajax        = Validator::queryParams($request)->boolean('ajax', false);
20506a438b4SGreg Roach
206b55cbc6bSGreg Roach        if ($ajax) {
20706a438b4SGreg Roach            $this->layout = 'layouts/ajax';
20806a438b4SGreg Roach
20906a438b4SGreg Roach            // Highlight direct-line ancestors of this individual.
2101fe542e9SGreg Roach            $xref = $tree->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF);
2116b9cb339SGreg Roach            $self = Registry::individualFactory()->make($xref, $tree);
21206a438b4SGreg Roach
21306a438b4SGreg Roach            if ($surname !== '') {
21406a438b4SGreg Roach                $individuals = $this->loadIndividuals($tree, $surname, $soundex_dm, $soundex_std);
21506a438b4SGreg Roach            } else {
21606a438b4SGreg Roach                $individuals = [];
21706a438b4SGreg Roach            }
21806a438b4SGreg Roach
21906a438b4SGreg Roach            if ($self instanceof Individual) {
22006a438b4SGreg Roach                $ancestors = $this->allAncestors($self);
22106a438b4SGreg Roach            } else {
22206a438b4SGreg Roach                $ancestors = [];
22306a438b4SGreg Roach            }
22406a438b4SGreg Roach
22506a438b4SGreg Roach            return $this->viewResponse('modules/branches/list', [
22606a438b4SGreg Roach                'branches' => $this->getPatriarchsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std),
22706a438b4SGreg Roach            ]);
22806a438b4SGreg Roach        }
22906a438b4SGreg Roach
23006a438b4SGreg Roach        if ($surname !== '') {
23106a438b4SGreg Roach            /* I18N: %s is a surname */
23206a438b4SGreg Roach            $title = I18N::translate('Branches of the %s family', e($surname));
23306a438b4SGreg Roach
234748dbe15SGreg Roach            $ajax_url = $this->listUrl($tree, [
235748dbe15SGreg Roach                'ajax'        => true,
236748dbe15SGreg Roach                'soundex_dm'  => $soundex_dm,
237748dbe15SGreg Roach                'soundex_std' => $soundex_std,
238748dbe15SGreg Roach                'surname'     => $surname,
239748dbe15SGreg Roach            ]);
24006a438b4SGreg Roach        } else {
24106a438b4SGreg Roach            /* I18N: Branches of a family tree */
24206a438b4SGreg Roach            $title = I18N::translate('Branches');
24306a438b4SGreg Roach
24406a438b4SGreg Roach            $ajax_url = '';
24506a438b4SGreg Roach        }
24606a438b4SGreg Roach
24706a438b4SGreg Roach        return $this->viewResponse('branches-page', [
24806a438b4SGreg Roach            'ajax_url'    => $ajax_url,
24906a438b4SGreg Roach            'soundex_dm'  => $soundex_dm,
25006a438b4SGreg Roach            'soundex_std' => $soundex_std,
25106a438b4SGreg Roach            'surname'     => $surname,
25206a438b4SGreg Roach            'title'       => $title,
25306a438b4SGreg Roach            'tree'        => $tree,
25406a438b4SGreg Roach        ]);
25506a438b4SGreg Roach    }
25606a438b4SGreg Roach
25706a438b4SGreg Roach    /**
25806a438b4SGreg Roach     * Find all ancestors of an individual, indexed by the Sosa-Stradonitz number.
25906a438b4SGreg Roach     *
26006a438b4SGreg Roach     * @param Individual $individual
26106a438b4SGreg Roach     *
26209482a55SGreg Roach     * @return array<Individual>
26306a438b4SGreg Roach     */
26406a438b4SGreg Roach    private function allAncestors(Individual $individual): array
26506a438b4SGreg Roach    {
26606a438b4SGreg Roach        $ancestors = [
26706a438b4SGreg Roach            1 => $individual,
26806a438b4SGreg Roach        ];
26906a438b4SGreg Roach
27006a438b4SGreg Roach        do {
27106a438b4SGreg Roach            $sosa = key($ancestors);
27206a438b4SGreg Roach
27306a438b4SGreg Roach            $family = $ancestors[$sosa]->childFamilies()->first();
27406a438b4SGreg Roach
27506a438b4SGreg Roach            if ($family !== null) {
27606a438b4SGreg Roach                if ($family->husband() !== null) {
27706a438b4SGreg Roach                    $ancestors[$sosa * 2] = $family->husband();
27806a438b4SGreg Roach                }
27906a438b4SGreg Roach                if ($family->wife() !== null) {
28006a438b4SGreg Roach                    $ancestors[$sosa * 2 + 1] = $family->wife();
28106a438b4SGreg Roach                }
28206a438b4SGreg Roach            }
28306a438b4SGreg Roach        } while (next($ancestors));
28406a438b4SGreg Roach
28506a438b4SGreg Roach        return $ancestors;
28606a438b4SGreg Roach    }
28706a438b4SGreg Roach
28806a438b4SGreg Roach    /**
28906a438b4SGreg Roach     * Fetch all individuals with a matching surname
29006a438b4SGreg Roach     *
29106a438b4SGreg Roach     * @param Tree   $tree
29206a438b4SGreg Roach     * @param string $surname
29306a438b4SGreg Roach     * @param bool   $soundex_dm
29406a438b4SGreg Roach     * @param bool   $soundex_std
29506a438b4SGreg Roach     *
29609482a55SGreg Roach     * @return array<Individual>
29706a438b4SGreg Roach     */
29806a438b4SGreg Roach    private function loadIndividuals(Tree $tree, string $surname, bool $soundex_dm, bool $soundex_std): array
29906a438b4SGreg Roach    {
30006a438b4SGreg Roach        $individuals = DB::table('individuals')
30106a438b4SGreg Roach            ->join('name', static function (JoinClause $join): void {
30206a438b4SGreg Roach                $join
30306a438b4SGreg Roach                    ->on('name.n_file', '=', 'individuals.i_file')
30406a438b4SGreg Roach                    ->on('name.n_id', '=', 'individuals.i_id');
30506a438b4SGreg Roach            })
30606a438b4SGreg Roach            ->where('i_file', '=', $tree->id())
30706a438b4SGreg Roach            ->where('n_type', '<>', '_MARNM')
30806a438b4SGreg Roach            ->where(static function (Builder $query) use ($surname, $soundex_dm, $soundex_std): void {
30906a438b4SGreg Roach                $query
31006a438b4SGreg Roach                    ->where('n_surn', '=', $surname)
31106a438b4SGreg Roach                    ->orWhere('n_surname', '=', $surname);
31206a438b4SGreg Roach
31306a438b4SGreg Roach                if ($soundex_std) {
31406a438b4SGreg Roach                    $sdx = Soundex::russell($surname);
31506a438b4SGreg Roach                    if ($sdx !== '') {
31606a438b4SGreg Roach                        foreach (explode(':', $sdx) as $value) {
31706a438b4SGreg Roach                            $query->orWhere('n_soundex_surn_std', 'LIKE', '%' . $value . '%');
31806a438b4SGreg Roach                        }
31906a438b4SGreg Roach                    }
32006a438b4SGreg Roach                }
32106a438b4SGreg Roach
32206a438b4SGreg Roach                if ($soundex_dm) {
32306a438b4SGreg Roach                    $sdx = Soundex::daitchMokotoff($surname);
32406a438b4SGreg Roach                    if ($sdx !== '') {
32506a438b4SGreg Roach                        foreach (explode(':', $sdx) as $value) {
32606a438b4SGreg Roach                            $query->orWhere('n_soundex_surn_dm', 'LIKE', '%' . $value . '%');
32706a438b4SGreg Roach                        }
32806a438b4SGreg Roach                    }
32906a438b4SGreg Roach                }
33006a438b4SGreg Roach            })
33106a438b4SGreg Roach            ->distinct()
332*0aed7de4SGreg Roach            ->select(['individuals.*'])
33306a438b4SGreg Roach            ->get()
3346b9cb339SGreg Roach            ->map(Registry::individualFactory()->mapper($tree))
33506a438b4SGreg Roach            ->filter(GedcomRecord::accessFilter())
33606a438b4SGreg Roach            ->all();
33706a438b4SGreg Roach
33806a438b4SGreg Roach        usort($individuals, Individual::birthDateComparator());
33906a438b4SGreg Roach
34006a438b4SGreg Roach        return $individuals;
34106a438b4SGreg Roach    }
34206a438b4SGreg Roach
34306a438b4SGreg Roach    /**
34406a438b4SGreg Roach     * For each individual with no ancestors, list their descendants.
34506a438b4SGreg Roach     *
34606a438b4SGreg Roach     * @param Tree              $tree
34709482a55SGreg Roach     * @param array<Individual> $individuals
34809482a55SGreg Roach     * @param array<Individual> $ancestors
34906a438b4SGreg Roach     * @param string            $surname
35006a438b4SGreg Roach     * @param bool              $soundex_dm
35106a438b4SGreg Roach     * @param bool              $soundex_std
35206a438b4SGreg Roach     *
35306a438b4SGreg Roach     * @return string
35406a438b4SGreg Roach     */
35506a438b4SGreg Roach    private function getPatriarchsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std): string
35606a438b4SGreg Roach    {
35706a438b4SGreg Roach        $html = '';
35806a438b4SGreg Roach        foreach ($individuals as $individual) {
35906a438b4SGreg Roach            foreach ($individual->childFamilies() as $family) {
36006a438b4SGreg Roach                foreach ($family->spouses() as $parent) {
36106a438b4SGreg Roach                    if (in_array($parent, $individuals, true)) {
36206a438b4SGreg Roach                        continue 3;
36306a438b4SGreg Roach                    }
36406a438b4SGreg Roach                }
36506a438b4SGreg Roach            }
36606a438b4SGreg Roach            $html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $individual, null);
36706a438b4SGreg Roach        }
36806a438b4SGreg Roach
36906a438b4SGreg Roach        return $html;
37006a438b4SGreg Roach    }
37106a438b4SGreg Roach
37206a438b4SGreg Roach    /**
37306a438b4SGreg Roach     * Generate a recursive list of descendants of an individual.
37406a438b4SGreg Roach     * If parents are specified, we can also show the pedigree (adopted, etc.).
37506a438b4SGreg Roach     *
37606a438b4SGreg Roach     * @param Tree              $tree
37709482a55SGreg Roach     * @param array<Individual> $individuals
37809482a55SGreg Roach     * @param array<Individual> $ancestors
37906a438b4SGreg Roach     * @param string            $surname
38006a438b4SGreg Roach     * @param bool              $soundex_dm
38106a438b4SGreg Roach     * @param bool              $soundex_std
38206a438b4SGreg Roach     * @param Individual        $individual
38306a438b4SGreg Roach     * @param Family|null       $parents
38406a438b4SGreg Roach     *
38506a438b4SGreg Roach     * @return string
38606a438b4SGreg Roach     */
38706a438b4SGreg Roach    private function getDescendantsHtml(Tree $tree, array $individuals, array $ancestors, string $surname, bool $soundex_dm, bool $soundex_std, Individual $individual, Family $parents = null): string
38806a438b4SGreg Roach    {
38906a438b4SGreg Roach        $module = $this->module_service->findByComponent(ModuleChartInterface::class, $tree, Auth::user())->first(static function (ModuleInterface $module) {
39006a438b4SGreg Roach            return $module instanceof RelationshipsChartModule;
39106a438b4SGreg Roach        });
39206a438b4SGreg Roach
39306a438b4SGreg Roach        // A person has many names. Select the one that matches the searched surname
39406a438b4SGreg Roach        $person_name = '';
39506a438b4SGreg Roach        foreach ($individual->getAllNames() as $name) {
39606a438b4SGreg Roach            [$surn1] = explode(',', $name['sort']);
39706a438b4SGreg Roach            if ($this->surnamesMatch($surn1, $surname, $soundex_std, $soundex_dm)) {
39806a438b4SGreg Roach                $person_name = $name['full'];
39906a438b4SGreg Roach                break;
40006a438b4SGreg Roach            }
40106a438b4SGreg Roach        }
40206a438b4SGreg Roach
40306a438b4SGreg Roach        // No matching name? Typically children with a different surname. The branch stops here.
40406a438b4SGreg Roach        if ($person_name === '') {
405f773eef0SGreg Roach            return '<li title="' . strip_tags($individual->fullName()) . '" class="wt-branch-split"><small>' . view('icons/sex', ['sex' => $individual->sex()]) . '</small>…</li>';
40606a438b4SGreg Roach        }
40706a438b4SGreg Roach
40806a438b4SGreg Roach        // Is this individual one of our ancestors?
40906a438b4SGreg Roach        $sosa = array_search($individual, $ancestors, true);
41006a438b4SGreg Roach        if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
41106a438b4SGreg Roach            $sosa_class = 'search_hit';
412382922aaSGreg Roach            $sosa_html  = ' <a class="small wt-chart-box-' . strtolower($individual->sex()) . '" href="' . e($module->chartUrl($individual, ['xref2' => $ancestors[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationship') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
41306a438b4SGreg Roach        } else {
41406a438b4SGreg Roach            $sosa_class = '';
41506a438b4SGreg Roach            $sosa_html  = '';
41606a438b4SGreg Roach        }
41706a438b4SGreg Roach
41806a438b4SGreg Roach        // Generate HTML for this individual, and all their descendants
41906a438b4SGreg Roach        $indi_html = '<small>' . view('icons/sex', ['sex' => $individual->sex()]) . '</small><a class="' . $sosa_class . '" href="' . e($individual->url()) . '">' . $person_name . '</a> ' . $individual->lifespan() . $sosa_html;
42006a438b4SGreg Roach
42106a438b4SGreg Roach        // If this is not a birth pedigree (e.g. an adoption), highlight it
42206a438b4SGreg Roach        if ($parents) {
42306a438b4SGreg Roach            foreach ($individual->facts(['FAMC']) as $fact) {
42406a438b4SGreg Roach                if ($fact->target() === $parents) {
42506a438b4SGreg Roach                    $pedi = $fact->attribute('PEDI');
426fd307386SGreg Roach
42788a03560SGreg Roach                    if ($pedi !== '' && $pedi !== PedigreeLinkageType::VALUE_BIRTH) {
428c2ed51d1SGreg Roach                        $pedigree  = Registry::elementFactory()->make('INDI:FAMC:PEDI')->value($pedi, $tree);
429fd307386SGreg Roach                        $indi_html = '<span class="red">' . $pedigree . '</span> ' . $indi_html;
430fd307386SGreg Roach                    }
43106a438b4SGreg Roach                    break;
43206a438b4SGreg Roach                }
43306a438b4SGreg Roach            }
43406a438b4SGreg Roach        }
43506a438b4SGreg Roach
43606a438b4SGreg Roach        // spouses and children
43706a438b4SGreg Roach        $spouse_families = $individual->spouseFamilies()
43806a438b4SGreg Roach            ->sort(Family::marriageDateComparator());
43906a438b4SGreg Roach
44006a438b4SGreg Roach        if ($spouse_families->isNotEmpty()) {
44106a438b4SGreg Roach            $fam_html = '';
44206a438b4SGreg Roach            foreach ($spouse_families as $family) {
44306a438b4SGreg Roach                $fam_html .= $indi_html; // Repeat the individual details for each spouse.
44406a438b4SGreg Roach
44506a438b4SGreg Roach                $spouse = $family->spouse($individual);
44606a438b4SGreg Roach                if ($spouse instanceof Individual) {
44706a438b4SGreg Roach                    $sosa = array_search($spouse, $ancestors, true);
44806a438b4SGreg Roach                    if (is_int($sosa) && $module instanceof RelationshipsChartModule) {
44906a438b4SGreg Roach                        $sosa_class = 'search_hit';
450382922aaSGreg Roach                        $sosa_html  = ' <a class="small wt-chart-box-' . strtolower($spouse->sex()) . '" href="' . e($module->chartUrl($spouse, ['xref2' => $ancestors[1]->xref()])) . '" rel="nofollow" title="' . I18N::translate('Relationship') . '">' . I18N::number($sosa) . '</a>' . self::sosaGeneration($sosa);
45106a438b4SGreg Roach                    } else {
45206a438b4SGreg Roach                        $sosa_class = '';
45306a438b4SGreg Roach                        $sosa_html  = '';
45406a438b4SGreg Roach                    }
45506a438b4SGreg Roach                    $marriage_year = $family->getMarriageYear();
45606a438b4SGreg Roach                    if ($marriage_year) {
45706a438b4SGreg Roach                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . strip_tags($family->getMarriageDate()->display()) . '"><i class="icon-rings"></i>' . $marriage_year . '</a>';
45806a438b4SGreg Roach                    } elseif ($family->facts(['MARR'])->isNotEmpty()) {
45906a438b4SGreg Roach                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Marriage') . '"><i class="icon-rings"></i></a>';
46006a438b4SGreg Roach                    } else {
46106a438b4SGreg Roach                        $fam_html .= ' <a href="' . e($family->url()) . '" title="' . I18N::translate('Not married') . '"><i class="icon-rings"></i></a>';
46206a438b4SGreg Roach                    }
46306a438b4SGreg Roach                    $fam_html .= ' <small>' . view('icons/sex', ['sex' => $spouse->sex()]) . '</small><a class="' . $sosa_class . '" href="' . e($spouse->url()) . '">' . $spouse->fullName() . '</a> ' . $spouse->lifespan() . ' ' . $sosa_html;
46406a438b4SGreg Roach                }
46506a438b4SGreg Roach
46606a438b4SGreg Roach                $fam_html .= '<ol>';
46706a438b4SGreg Roach                foreach ($family->children() as $child) {
46806a438b4SGreg Roach                    $fam_html .= $this->getDescendantsHtml($tree, $individuals, $ancestors, $surname, $soundex_dm, $soundex_std, $child, $family);
46906a438b4SGreg Roach                }
47006a438b4SGreg Roach                $fam_html .= '</ol>';
47106a438b4SGreg Roach            }
47206a438b4SGreg Roach
47306a438b4SGreg Roach            return '<li>' . $fam_html . '</li>';
47406a438b4SGreg Roach        }
47506a438b4SGreg Roach
47606a438b4SGreg Roach        // No spouses - just show the individual
47706a438b4SGreg Roach        return '<li>' . $indi_html . '</li>';
47806a438b4SGreg Roach    }
47906a438b4SGreg Roach
48006a438b4SGreg Roach    /**
48106a438b4SGreg Roach     * Do two surnames match?
48206a438b4SGreg Roach     *
48306a438b4SGreg Roach     * @param string $surname1
48406a438b4SGreg Roach     * @param string $surname2
48506a438b4SGreg Roach     * @param bool   $soundex_std
48606a438b4SGreg Roach     * @param bool   $soundex_dm
48706a438b4SGreg Roach     *
48806a438b4SGreg Roach     * @return bool
48906a438b4SGreg Roach     */
49006a438b4SGreg Roach    private function surnamesMatch(string $surname1, string $surname2, bool $soundex_std, bool $soundex_dm): bool
49106a438b4SGreg Roach    {
49206a438b4SGreg Roach        // One name sounds like another?
49306a438b4SGreg Roach        if ($soundex_std && Soundex::compare(Soundex::russell($surname1), Soundex::russell($surname2))) {
49406a438b4SGreg Roach            return true;
49506a438b4SGreg Roach        }
49606a438b4SGreg Roach        if ($soundex_dm && Soundex::compare(Soundex::daitchMokotoff($surname1), Soundex::daitchMokotoff($surname2))) {
49706a438b4SGreg Roach            return true;
49806a438b4SGreg Roach        }
49906a438b4SGreg Roach
50006a438b4SGreg Roach        // One is a substring of the other.  e.g. Halen / Van Halen
50106a438b4SGreg Roach        return stripos($surname1, $surname2) !== false || stripos($surname2, $surname1) !== false;
50206a438b4SGreg Roach    }
50306a438b4SGreg Roach
50406a438b4SGreg Roach    /**
50506a438b4SGreg Roach     * Convert a SOSA number into a generation number. e.g. 8 = great-grandfather = 3 generations
50606a438b4SGreg Roach     *
50706a438b4SGreg Roach     * @param int $sosa
50806a438b4SGreg Roach     *
50906a438b4SGreg Roach     * @return string
51006a438b4SGreg Roach     */
51124f2a3afSGreg Roach    private static function sosaGeneration(int $sosa): string
51206a438b4SGreg Roach    {
51306a438b4SGreg Roach        $generation = (int) log($sosa, 2) + 1;
51406a438b4SGreg Roach
51506a438b4SGreg Roach        return '<sup title="' . I18N::translate('Generation') . '">' . $generation . '</sup>';
51606a438b4SGreg Roach    }
51767992b6aSRichard Cissee}
518