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