100ef1d3aSGreg Roach<?php 200ef1d3aSGreg Roach 300ef1d3aSGreg Roach/** 400ef1d3aSGreg Roach * webtrees: online genealogy 500ef1d3aSGreg Roach * Copyright (C) 2023 webtrees development team 600ef1d3aSGreg Roach * This program is free software: you can redistribute it and/or modify 700ef1d3aSGreg Roach * it under the terms of the GNU General Public License as published by 800ef1d3aSGreg Roach * the Free Software Foundation, either version 3 of the License, or 900ef1d3aSGreg Roach * (at your option) any later version. 1000ef1d3aSGreg Roach * This program is distributed in the hope that it will be useful, 1100ef1d3aSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1200ef1d3aSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1300ef1d3aSGreg Roach * GNU General Public License for more details. 1400ef1d3aSGreg Roach * You should have received a copy of the GNU General Public License 1500ef1d3aSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 1600ef1d3aSGreg Roach */ 1700ef1d3aSGreg Roach 1800ef1d3aSGreg Roachdeclare(strict_types=1); 1900ef1d3aSGreg Roach 2000ef1d3aSGreg Roachnamespace Fisharebest\Webtrees\Module; 2100ef1d3aSGreg Roach 2200ef1d3aSGreg Roachuse Fig\Http\Message\StatusCodeInterface; 2300ef1d3aSGreg Roachuse Fisharebest\Webtrees\Auth; 2400ef1d3aSGreg Roachuse Fisharebest\Webtrees\DB; 2500ef1d3aSGreg Roachuse Fisharebest\Webtrees\Family; 2600ef1d3aSGreg Roachuse Fisharebest\Webtrees\FlashMessages; 2700ef1d3aSGreg Roachuse Fisharebest\Webtrees\I18N; 2800ef1d3aSGreg Roachuse Fisharebest\Webtrees\Individual; 2900ef1d3aSGreg Roachuse Fisharebest\Webtrees\Registry; 3000ef1d3aSGreg Roachuse Fisharebest\Webtrees\Session; 3100ef1d3aSGreg Roachuse Fisharebest\Webtrees\Tree; 3200ef1d3aSGreg Roachuse Fisharebest\Webtrees\Validator; 3300ef1d3aSGreg Roachuse Illuminate\Database\Query\Builder; 3400ef1d3aSGreg Roachuse Illuminate\Database\Query\Expression; 3500ef1d3aSGreg Roachuse Illuminate\Database\Query\JoinClause; 3600ef1d3aSGreg Roachuse Illuminate\Support\Collection; 3700ef1d3aSGreg Roachuse Psr\Http\Message\ResponseInterface; 3800ef1d3aSGreg Roachuse Psr\Http\Message\ServerRequestInterface; 3900ef1d3aSGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 4000ef1d3aSGreg Roach 4100ef1d3aSGreg Roachuse function array_filter; 4200ef1d3aSGreg Roachuse function array_key_exists; 4300ef1d3aSGreg Roachuse function array_keys; 4400ef1d3aSGreg Roachuse function array_map; 4500ef1d3aSGreg Roachuse function array_merge; 4600ef1d3aSGreg Roachuse function array_sum; 4700ef1d3aSGreg Roachuse function array_values; 4800ef1d3aSGreg Roachuse function assert; 4900ef1d3aSGreg Roachuse function e; 5000ef1d3aSGreg Roachuse function implode; 5100ef1d3aSGreg Roachuse function ob_get_clean; 5200ef1d3aSGreg Roachuse function ob_start; 5300ef1d3aSGreg Roachuse function route; 5400ef1d3aSGreg Roachuse function uksort; 5500ef1d3aSGreg Roachuse function usort; 5600ef1d3aSGreg Roachuse function view; 5700ef1d3aSGreg Roach 5800ef1d3aSGreg Roachuse const ARRAY_FILTER_USE_KEY; 5900ef1d3aSGreg Roach 6000ef1d3aSGreg Roach/** 6100ef1d3aSGreg Roach * Common logic for individual and family lists. 6200ef1d3aSGreg Roach */ 6300ef1d3aSGreg Roachabstract class AbstractIndividualListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface 6400ef1d3aSGreg Roach{ 6500ef1d3aSGreg Roach use ModuleListTrait; 6600ef1d3aSGreg Roach 6700ef1d3aSGreg Roach // The individual list and family list use the same code/logic. 6800ef1d3aSGreg Roach // They just display different lists. 6900ef1d3aSGreg Roach abstract protected function showFamilies(): bool; 7000ef1d3aSGreg Roach 7100ef1d3aSGreg Roach abstract protected function routeUrl(): string; 7200ef1d3aSGreg Roach 7300ef1d3aSGreg Roach /** 7400ef1d3aSGreg Roach * Initialization. 7500ef1d3aSGreg Roach */ 7600ef1d3aSGreg Roach public function boot(): void 7700ef1d3aSGreg Roach { 7800ef1d3aSGreg Roach Registry::routeFactory()->routeMap()->get(static::class, $this->routeUrl(), $this); 7900ef1d3aSGreg Roach } 8000ef1d3aSGreg Roach 8100ef1d3aSGreg Roach /** 8200ef1d3aSGreg Roach * @param array<bool|int|string|array<string>|null> $parameters 8300ef1d3aSGreg Roach */ 8400ef1d3aSGreg Roach public function listUrl(Tree $tree, array $parameters = []): string 8500ef1d3aSGreg Roach { 8600ef1d3aSGreg Roach $request = Registry::container()->get(ServerRequestInterface::class); 8700ef1d3aSGreg Roach $xref = Validator::attributes($request)->isXref()->string('xref', ''); 8800ef1d3aSGreg Roach 8900ef1d3aSGreg Roach if ($xref !== '') { 9000ef1d3aSGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 9100ef1d3aSGreg Roach 9200ef1d3aSGreg Roach if ($individual instanceof Individual && $individual->canShow()) { 9300ef1d3aSGreg Roach $primary_name = $individual->getPrimaryName(); 9400ef1d3aSGreg Roach 9500ef1d3aSGreg Roach $parameters['surname'] ??= $individual->getAllNames()[$primary_name]['surn'] ?? null; 9600ef1d3aSGreg Roach } 9700ef1d3aSGreg Roach } 9800ef1d3aSGreg Roach 9900ef1d3aSGreg Roach $parameters['tree'] = $tree->name(); 10000ef1d3aSGreg Roach 10100ef1d3aSGreg Roach return route(static::class, $parameters); 10200ef1d3aSGreg Roach } 10300ef1d3aSGreg Roach 10400ef1d3aSGreg Roach /** 10500ef1d3aSGreg Roach * @return array<string> 10600ef1d3aSGreg Roach */ 10700ef1d3aSGreg Roach public function listUrlAttributes(): array 10800ef1d3aSGreg Roach { 10900ef1d3aSGreg Roach return []; 11000ef1d3aSGreg Roach } 11100ef1d3aSGreg Roach 11200ef1d3aSGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 11300ef1d3aSGreg Roach { 11400ef1d3aSGreg Roach $tree = Validator::attributes($request)->tree(); 11500ef1d3aSGreg Roach $user = Validator::attributes($request)->user(); 11600ef1d3aSGreg Roach 11700ef1d3aSGreg Roach Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 11800ef1d3aSGreg Roach 11900ef1d3aSGreg Roach // All individuals with this surname 12000ef1d3aSGreg Roach $surname_param = Validator::queryParams($request)->string('surname', ''); 12100ef1d3aSGreg Roach $surname = I18N::strtoupper(I18N::language()->normalize($surname_param)); 12200ef1d3aSGreg Roach 12300ef1d3aSGreg Roach // All surnames beginning with this letter, where "@" is unknown and "," is none 12400ef1d3aSGreg Roach $alpha = Validator::queryParams($request)->string('alpha', ''); 12500ef1d3aSGreg Roach 12600ef1d3aSGreg Roach // All first names beginning with this letter where "@" is unknown 12700ef1d3aSGreg Roach $falpha = Validator::queryParams($request)->string('falpha', ''); 12800ef1d3aSGreg Roach 12900ef1d3aSGreg Roach // What type of list to display, if any 13000ef1d3aSGreg Roach $show = Validator::queryParams($request)->string('show', 'surn'); 13100ef1d3aSGreg Roach 13200ef1d3aSGreg Roach // All individuals 13300ef1d3aSGreg Roach $show_all = Validator::queryParams($request)->string('show_all', ''); 13400ef1d3aSGreg Roach 13500ef1d3aSGreg Roach // Include/exclude married names 13600ef1d3aSGreg Roach $show_marnm = Validator::queryParams($request)->string('show_marnm', ''); 13700ef1d3aSGreg Roach 13800ef1d3aSGreg Roach // Break long lists down by given name 13900ef1d3aSGreg Roach $show_all_firstnames = Validator::queryParams($request)->string('show_all_firstnames', ''); 14000ef1d3aSGreg Roach 14100ef1d3aSGreg Roach $params = [ 14200ef1d3aSGreg Roach 'alpha' => $alpha, 14300ef1d3aSGreg Roach 'falpha' => $falpha, 14400ef1d3aSGreg Roach 'show' => $show, 14500ef1d3aSGreg Roach 'show_all' => $show_all, 14600ef1d3aSGreg Roach 'show_all_firstnames' => $show_all_firstnames, 14700ef1d3aSGreg Roach 'show_marnm' => $show_marnm, 14800ef1d3aSGreg Roach 'surname' => $surname, 14900ef1d3aSGreg Roach ]; 15000ef1d3aSGreg Roach 15100ef1d3aSGreg Roach if ($surname_param !== $surname) { 15200ef1d3aSGreg Roach return Registry::responseFactory() 15300ef1d3aSGreg Roach ->redirectUrl($this->listUrl($tree, $params), StatusCodeInterface::STATUS_MOVED_PERMANENTLY); 15400ef1d3aSGreg Roach } 15500ef1d3aSGreg Roach 15600ef1d3aSGreg Roach // Make sure parameters are consistent with each other. 15700ef1d3aSGreg Roach if ($show_all_firstnames === 'yes') { 15800ef1d3aSGreg Roach $falpha = ''; 15900ef1d3aSGreg Roach } 16000ef1d3aSGreg Roach 16100ef1d3aSGreg Roach if ($show_all === 'yes') { 16200ef1d3aSGreg Roach $alpha = ''; 16300ef1d3aSGreg Roach $surname = ''; 16400ef1d3aSGreg Roach } 16500ef1d3aSGreg Roach 16600ef1d3aSGreg Roach if ($surname !== '') { 16700ef1d3aSGreg Roach $alpha = I18N::language()->initialLetter($surname); 16800ef1d3aSGreg Roach } 16900ef1d3aSGreg Roach 17000ef1d3aSGreg Roach $surname_data = $this->surnameData($tree, $show_marnm === 'yes', $this->showFamilies()); 17100ef1d3aSGreg Roach $all_surns = $this->allSurns($surname_data); 17200ef1d3aSGreg Roach $all_surnames = $this->allSurnames($surname_data); 17300ef1d3aSGreg Roach $surname_initials = $this->surnameInitials($surname_data); 17400ef1d3aSGreg Roach 17500ef1d3aSGreg Roach // We've requested a surname that doesn't currently exist. 17600ef1d3aSGreg Roach if ($surname !== '' && !array_key_exists($surname, $all_surns)) { 17700ef1d3aSGreg Roach $message = I18N::translate('There are no individuals with the surname “%s”', e($surname)); 17800ef1d3aSGreg Roach FlashMessages::addMessage($message); 17900ef1d3aSGreg Roach 18000ef1d3aSGreg Roach return Registry::responseFactory() 18100ef1d3aSGreg Roach ->redirectUrl($this->listUrl($tree)); 18200ef1d3aSGreg Roach } 18300ef1d3aSGreg Roach 18400ef1d3aSGreg Roach // Make sure selections are consistent. 18500ef1d3aSGreg Roach // i.e. can’t specify show_all and surname at the same time. 18600ef1d3aSGreg Roach if ($show_all === 'yes') { 18700ef1d3aSGreg Roach if ($show_all_firstnames === 'yes') { 18800ef1d3aSGreg Roach $legend = I18N::translate('All'); 18900ef1d3aSGreg Roach $params = ['tree' => $tree->name(), 'show_all' => 'yes', 'show_marnm' => $show_marnm]; 19000ef1d3aSGreg Roach $show = 'indi'; 19100ef1d3aSGreg Roach } elseif ($falpha !== '') { 19200ef1d3aSGreg Roach $legend = I18N::translate('All') . ', ' . e($falpha) . '…'; 19300ef1d3aSGreg Roach $params = ['tree' => $tree->name(), 'show_all' => 'yes', 'show_marnm' => $show_marnm]; 19400ef1d3aSGreg Roach $show = 'indi'; 19500ef1d3aSGreg Roach } else { 19600ef1d3aSGreg Roach $legend = I18N::translate('All'); 19700ef1d3aSGreg Roach $params = ['tree' => $tree->name(), 'show_all' => 'yes', 'show_marnm' => $show_marnm]; 19800ef1d3aSGreg Roach } 19900ef1d3aSGreg Roach } elseif ($surname !== '') { 20000ef1d3aSGreg Roach $show_all = 'no'; 20100ef1d3aSGreg Roach $show = 'indi'; // The surname list makes no sense with only one surname. 20200ef1d3aSGreg Roach $params = ['tree' => $tree->name(), 'surname' => $surname, 'falpha' => $falpha, 'show_marnm' => $show_marnm]; 20300ef1d3aSGreg Roach 20400ef1d3aSGreg Roach if ($surname === Individual::NOMEN_NESCIO) { 20500ef1d3aSGreg Roach $legend = I18N::translateContext('Unknown surname', '…'); 20600ef1d3aSGreg Roach } else { 20700ef1d3aSGreg Roach // The surname parameter is a root/canonical form. Display the actual surnames found. 20800ef1d3aSGreg Roach $variants = array_keys($all_surnames[$surname] ?? [$surname => $surname]); 20900ef1d3aSGreg Roach usort($variants, I18N::comparator()); 21000ef1d3aSGreg Roach $variants = array_map(static fn (string $x): string => $x === '' ? I18N::translate('No surname') : $x, $variants); 21100ef1d3aSGreg Roach $legend = implode('/', $variants); 21200ef1d3aSGreg Roach } 21300ef1d3aSGreg Roach 21400ef1d3aSGreg Roach switch ($falpha) { 21500ef1d3aSGreg Roach case '': 21600ef1d3aSGreg Roach break; 21700ef1d3aSGreg Roach case '@': 21800ef1d3aSGreg Roach $legend .= ', ' . I18N::translateContext('Unknown given name', '…'); 21900ef1d3aSGreg Roach break; 22000ef1d3aSGreg Roach default: 22100ef1d3aSGreg Roach $legend .= ', ' . e($falpha) . '…'; 22200ef1d3aSGreg Roach break; 22300ef1d3aSGreg Roach } 22400ef1d3aSGreg Roach } elseif ($alpha === '@') { 22500ef1d3aSGreg Roach $show_all = 'no'; 22600ef1d3aSGreg Roach $legend = I18N::translateContext('Unknown surname', '…'); 22700ef1d3aSGreg Roach $params = ['alpha' => $alpha, 'tree' => $tree->name(), 'show_marnm' => $show_marnm]; 22800ef1d3aSGreg Roach $surname = Individual::NOMEN_NESCIO; 22900ef1d3aSGreg Roach $show = 'indi'; // SURN list makes no sense here 23000ef1d3aSGreg Roach } elseif ($alpha === ',') { 23100ef1d3aSGreg Roach $show_all = 'no'; 23200ef1d3aSGreg Roach $legend = I18N::translate('No surname'); 23300ef1d3aSGreg Roach $params = ['alpha' => $alpha, 'tree' => $tree->name(), 'show_marnm' => $show_marnm]; 23400ef1d3aSGreg Roach $show = 'indi'; // SURN list makes no sense here 23500ef1d3aSGreg Roach } elseif ($alpha !== '') { 23600ef1d3aSGreg Roach $show_all = 'no'; 23700ef1d3aSGreg Roach $legend = e($alpha) . '…'; 23800ef1d3aSGreg Roach $params = ['alpha' => $alpha, 'tree' => $tree->name(), 'show_marnm' => $show_marnm]; 23900ef1d3aSGreg Roach } else { 24000ef1d3aSGreg Roach $show_all = 'no'; 24100ef1d3aSGreg Roach $legend = '…'; 24200ef1d3aSGreg Roach $params = ['tree' => $tree->name(), 'show_marnm' => $show_marnm]; 24300ef1d3aSGreg Roach $show = 'none'; // Don't show lists until something is chosen 24400ef1d3aSGreg Roach } 24500ef1d3aSGreg Roach $legend = '<bdi>' . $legend . '</bdi>'; 24600ef1d3aSGreg Roach 24700ef1d3aSGreg Roach if ($this->showFamilies()) { 24800ef1d3aSGreg Roach $title = I18N::translate('Families') . ' — ' . $legend; 24900ef1d3aSGreg Roach } else { 25000ef1d3aSGreg Roach $title = I18N::translate('Individuals') . ' — ' . $legend; 25100ef1d3aSGreg Roach } 25200ef1d3aSGreg Roach 25300ef1d3aSGreg Roach ob_start(); ?> 25400ef1d3aSGreg Roach <div class="d-flex flex-column wt-page-options wt-page-options-individual-list d-print-none"> 25500ef1d3aSGreg Roach <ul class="d-flex flex-wrap list-unstyled justify-content-center wt-initials-list wt-initials-list-surname"> 25600ef1d3aSGreg Roach 25700ef1d3aSGreg Roach <?php foreach ($surname_initials as $letter => $count) : ?> 25800ef1d3aSGreg Roach <li class="wt-initials-list-item d-flex"> 25900ef1d3aSGreg Roach <?php if ($count > 0) : ?> 26000ef1d3aSGreg Roach <a href="<?= e($this->listUrl($tree, ['alpha' => $letter, 'show_marnm' => $show_marnm, 'tree' => $tree->name()])) ?>" class="wt-initial px-1<?= $letter === $alpha ? ' active' : '' ?> '" title="<?= I18N::number($count) ?>"><?= $this->displaySurnameInitial((string) $letter) ?></a> 26100ef1d3aSGreg Roach <?php else : ?> 26200ef1d3aSGreg Roach <span class="wt-initial px-1 text-muted"><?= $this->displaySurnameInitial((string) $letter) ?></span> 26300ef1d3aSGreg Roach 26400ef1d3aSGreg Roach <?php endif ?> 26500ef1d3aSGreg Roach </li> 26600ef1d3aSGreg Roach <?php endforeach ?> 26700ef1d3aSGreg Roach 26800ef1d3aSGreg Roach <?php if (Session::has('initiated')) : ?> 26900ef1d3aSGreg Roach <!-- Search spiders don't get the "show all" option as the other links give them everything. --> 27000ef1d3aSGreg Roach <li class="wt-initials-list-item d-flex"> 27100ef1d3aSGreg Roach <a class="wt-initial px-1<?= $show_all === 'yes' ? ' active' : '' ?>" href="<?= e($this->listUrl($tree, ['show_all' => 'yes'] + $params)) ?>"><?= I18N::translate('All') ?></a> 27200ef1d3aSGreg Roach </li> 27300ef1d3aSGreg Roach <?php endif ?> 27400ef1d3aSGreg Roach </ul> 27500ef1d3aSGreg Roach 27600ef1d3aSGreg Roach <!-- Search spiders don't get an option to show/hide the surname sublists, nor does it make sense on the all/unknown/surname views --> 27700ef1d3aSGreg Roach <?php if ($show !== 'none' && Session::has('initiated')) : ?> 27800ef1d3aSGreg Roach <?php if ($show_marnm === 'yes') : ?> 27900ef1d3aSGreg Roach <p> 28000ef1d3aSGreg Roach <a href="<?= e($this->listUrl($tree, ['show' => $show, 'show_marnm' => 'no'] + $params)) ?>"> 28100ef1d3aSGreg Roach <?= I18N::translate('Exclude individuals with “%s” as a married name', $legend) ?> 28200ef1d3aSGreg Roach </a> 28300ef1d3aSGreg Roach </p> 28400ef1d3aSGreg Roach <?php else : ?> 28500ef1d3aSGreg Roach <p> 28600ef1d3aSGreg Roach <a href="<?= e($this->listUrl($tree, ['show' => $show, 'show_marnm' => 'yes'] + $params)) ?>"> 28700ef1d3aSGreg Roach <?= I18N::translate('Include individuals with “%s” as a married name', $legend) ?> 28800ef1d3aSGreg Roach </a> 28900ef1d3aSGreg Roach </p> 29000ef1d3aSGreg Roach <?php endif ?> 29100ef1d3aSGreg Roach 29200ef1d3aSGreg Roach <?php if ($alpha !== '@' && $alpha !== ',' && $surname === '') : ?> 29300ef1d3aSGreg Roach <?php if ($show === 'surn') : ?> 29400ef1d3aSGreg Roach <p> 29500ef1d3aSGreg Roach <a href="<?= e($this->listUrl($tree, ['show' => 'indi'] + $params)) ?>"> 29600ef1d3aSGreg Roach <?= I18N::translate('Show the list of individuals') ?> 29700ef1d3aSGreg Roach </a> 29800ef1d3aSGreg Roach </p> 29900ef1d3aSGreg Roach <?php else : ?> 30000ef1d3aSGreg Roach <p> 30100ef1d3aSGreg Roach <a href="<?= e($this->listUrl($tree, ['show' => 'surn'] + $params)) ?>"> 30200ef1d3aSGreg Roach <?= I18N::translate('Show the list of surnames') ?> 30300ef1d3aSGreg Roach </a> 30400ef1d3aSGreg Roach </p> 30500ef1d3aSGreg Roach <?php endif ?> 30600ef1d3aSGreg Roach <?php endif ?> 30700ef1d3aSGreg Roach <?php endif ?> 30800ef1d3aSGreg Roach </div> 30900ef1d3aSGreg Roach 31000ef1d3aSGreg Roach <div class="wt-page-content"> 311*c9eec602SGreg Roach <?php if ($show === 'indi' || $show === 'surn') { 31200ef1d3aSGreg Roach switch ($alpha) { 31300ef1d3aSGreg Roach case '@': 31400ef1d3aSGreg Roach $filter = static fn (string $x): bool => $x === Individual::NOMEN_NESCIO; 31500ef1d3aSGreg Roach break; 31600ef1d3aSGreg Roach case ',': 31700ef1d3aSGreg Roach $filter = static fn (string $x): bool => $x === ''; 31800ef1d3aSGreg Roach break; 31900ef1d3aSGreg Roach case '': 32000ef1d3aSGreg Roach if ($show_all === 'yes') { 32100ef1d3aSGreg Roach $filter = static fn (string $x): bool => $x !== '' && $x !== Individual::NOMEN_NESCIO; 32200ef1d3aSGreg Roach } else { 32300ef1d3aSGreg Roach $filter = static fn (string $x): bool => $x === $surname; 32400ef1d3aSGreg Roach } 32500ef1d3aSGreg Roach break; 32600ef1d3aSGreg Roach default: 32700ef1d3aSGreg Roach if ($surname === '') { 32800ef1d3aSGreg Roach $filter = static fn (string $x): bool => I18N::language()->initialLetter($x) === $alpha; 32900ef1d3aSGreg Roach } else { 33000ef1d3aSGreg Roach $filter = static fn (string $x): bool => $x === $surname; 33100ef1d3aSGreg Roach } 33200ef1d3aSGreg Roach break; 33300ef1d3aSGreg Roach } 33400ef1d3aSGreg Roach 33500ef1d3aSGreg Roach $all_surnames = array_filter($all_surnames, $filter, ARRAY_FILTER_USE_KEY); 33600ef1d3aSGreg Roach 33700ef1d3aSGreg Roach if ($show === 'surn') { 33800ef1d3aSGreg Roach // Show the surname list 33900ef1d3aSGreg Roach switch ($tree->getPreference('SURNAME_LIST_STYLE')) { 34000ef1d3aSGreg Roach case 'style1': 34100ef1d3aSGreg Roach echo view('lists/surnames-column-list', [ 34200ef1d3aSGreg Roach 'module' => $this, 34300ef1d3aSGreg Roach 'params' => ['show' => 'indi', 'show_all' => null] + $params, 34400ef1d3aSGreg Roach 'surnames' => $all_surnames, 34500ef1d3aSGreg Roach 'totals' => true, 34600ef1d3aSGreg Roach 'tree' => $tree, 34700ef1d3aSGreg Roach ]); 34800ef1d3aSGreg Roach break; 34900ef1d3aSGreg Roach case 'style3': 35000ef1d3aSGreg Roach echo view('lists/surnames-tag-cloud', [ 35100ef1d3aSGreg Roach 'module' => $this, 35200ef1d3aSGreg Roach 'params' => ['show' => 'indi', 'show_all' => null] + $params, 35300ef1d3aSGreg Roach 'surnames' => $all_surnames, 35400ef1d3aSGreg Roach 'totals' => true, 35500ef1d3aSGreg Roach 'tree' => $tree, 35600ef1d3aSGreg Roach ]); 35700ef1d3aSGreg Roach break; 35800ef1d3aSGreg Roach case 'style2': 35900ef1d3aSGreg Roach default: 36000ef1d3aSGreg Roach echo view('lists/surnames-table', [ 36100ef1d3aSGreg Roach 'families' => $this->showFamilies(), 36200ef1d3aSGreg Roach 'module' => $this, 36300ef1d3aSGreg Roach 'order' => [[0, 'asc']], 36400ef1d3aSGreg Roach 'params' => ['show' => 'indi', 'show_all' => null] + $params, 36500ef1d3aSGreg Roach 'surnames' => $all_surnames, 36600ef1d3aSGreg Roach 'tree' => $tree, 36700ef1d3aSGreg Roach ]); 36800ef1d3aSGreg Roach break; 36900ef1d3aSGreg Roach } 37000ef1d3aSGreg Roach } else { 37100ef1d3aSGreg Roach // Show the list 37200ef1d3aSGreg Roach $count = array_sum(array_map(static fn (array $x): int => array_sum($x), $all_surnames)); 37300ef1d3aSGreg Roach 37400ef1d3aSGreg Roach // Don't sublist short lists. 37500ef1d3aSGreg Roach $sublist_threshold = (int) $tree->getPreference('SUBLIST_TRIGGER_I'); 37600ef1d3aSGreg Roach if ($sublist_threshold === 0 || $count < $sublist_threshold) { 37700ef1d3aSGreg Roach $falpha = ''; 37800ef1d3aSGreg Roach } else { 37900ef1d3aSGreg Roach // Break long lists by initial letter of given name 38000ef1d3aSGreg Roach $all_surnames = array_values(array_map(static fn ($x): array => array_keys($x), $all_surnames)); 38100ef1d3aSGreg Roach $all_surnames = array_merge(...$all_surnames); 38200ef1d3aSGreg Roach $givn_initials = $this->givenNameInitials($tree, $all_surnames, $show_marnm === 'yes', $this->showFamilies()); 38300ef1d3aSGreg Roach 38400ef1d3aSGreg Roach if ($surname !== '' || $show_all === 'yes') { 38500ef1d3aSGreg Roach if ($show_all !== 'yes') { 38600ef1d3aSGreg Roach echo '<h2 class="wt-page-title">', I18N::translate('Individuals with surname %s', $legend), '</h2>'; 38700ef1d3aSGreg Roach } 38800ef1d3aSGreg Roach // Don't show the list until we have some filter criteria 38900ef1d3aSGreg Roach $show = $falpha !== '' || $show_all_firstnames === 'yes' ? 'indi' : 'none'; 39000ef1d3aSGreg Roach echo '<ul class="d-flex flex-wrap list-unstyled justify-content-center wt-initials-list wt-initials-list-given-names">'; 39100ef1d3aSGreg Roach foreach ($givn_initials as $givn_initial => $given_count) { 39200ef1d3aSGreg Roach echo '<li class="wt-initials-list-item d-flex">'; 39300ef1d3aSGreg Roach if ($given_count > 0) { 39400ef1d3aSGreg Roach if ($show === 'indi' && $givn_initial === $falpha && $show_all_firstnames !== 'yes') { 39500ef1d3aSGreg Roach echo '<a class="wt-initial px-1 active" href="' . e($this->listUrl($tree, ['falpha' => $givn_initial] + $params)) . '" title="' . I18N::number($given_count) . '">' . $this->displayGivenNameInitial((string) $givn_initial) . '</a>'; 39600ef1d3aSGreg Roach } else { 39700ef1d3aSGreg Roach echo '<a class="wt-initial px-1" href="' . e($this->listUrl($tree, ['falpha' => $givn_initial] + $params)) . '" title="' . I18N::number($given_count) . '">' . $this->displayGivenNameInitial((string) $givn_initial) . '</a>'; 39800ef1d3aSGreg Roach } 39900ef1d3aSGreg Roach } else { 40000ef1d3aSGreg Roach echo '<span class="wt-initial px-1 text-muted">' . $this->displayGivenNameInitial((string) $givn_initial) . '</span>'; 40100ef1d3aSGreg Roach } 40200ef1d3aSGreg Roach echo '</li>'; 40300ef1d3aSGreg Roach } 40400ef1d3aSGreg Roach // Search spiders don't get the "show all" option as the other links give them everything. 40500ef1d3aSGreg Roach if (Session::has('initiated')) { 40600ef1d3aSGreg Roach echo '<li class="wt-initials-list-item d-flex">'; 40700ef1d3aSGreg Roach if ($show_all_firstnames === 'yes') { 40800ef1d3aSGreg Roach echo '<span class="wt-initial px-1 active">' . I18N::translate('All') . '</span>'; 40900ef1d3aSGreg Roach } else { 41000ef1d3aSGreg Roach echo '<a class="wt-initial px-1" href="' . e($this->listUrl($tree, ['show_all_firstnames' => 'yes'] + $params)) . '" title="' . I18N::number($count) . '">' . I18N::translate('All') . '</a>'; 41100ef1d3aSGreg Roach } 41200ef1d3aSGreg Roach echo '</li>'; 41300ef1d3aSGreg Roach } 41400ef1d3aSGreg Roach echo '</ul>'; 41500ef1d3aSGreg Roach } 41600ef1d3aSGreg Roach } 41700ef1d3aSGreg Roach if ($show === 'indi') { 41800ef1d3aSGreg Roach if ($alpha === '@') { 41900ef1d3aSGreg Roach $surns_to_show = ['@N.N.']; 42000ef1d3aSGreg Roach } elseif ($alpha === ',') { 42100ef1d3aSGreg Roach $surns_to_show = ['']; 42200ef1d3aSGreg Roach } elseif ($surname !== '') { 42300ef1d3aSGreg Roach $surns_to_show = $all_surns[$surname]; 42400ef1d3aSGreg Roach } elseif ($alpha !== '') { 42500ef1d3aSGreg Roach $tmp = array_filter( 42600ef1d3aSGreg Roach $all_surns, 42700ef1d3aSGreg Roach static fn (string $x): bool => I18N::language()->initialLetter($x) === $alpha, 42800ef1d3aSGreg Roach ARRAY_FILTER_USE_KEY 42900ef1d3aSGreg Roach ); 43000ef1d3aSGreg Roach 43100ef1d3aSGreg Roach $surns_to_show = array_merge(...array_values($tmp)); 43200ef1d3aSGreg Roach } else { 43300ef1d3aSGreg Roach $surns_to_show = []; 43400ef1d3aSGreg Roach } 43500ef1d3aSGreg Roach 43600ef1d3aSGreg Roach if ($this->showFamilies()) { 43700ef1d3aSGreg Roach echo view('lists/families-table', [ 43800ef1d3aSGreg Roach 'families' => $this->families($tree, $surns_to_show, $falpha, $show_marnm === 'yes'), 43900ef1d3aSGreg Roach 'tree' => $tree, 44000ef1d3aSGreg Roach ]); 44100ef1d3aSGreg Roach } else { 44200ef1d3aSGreg Roach echo view('lists/individuals-table', [ 44300ef1d3aSGreg Roach 'individuals' => $this->individuals($tree, $surns_to_show, $falpha, $show_marnm === 'yes', false), 44400ef1d3aSGreg Roach 'sosa' => false, 44500ef1d3aSGreg Roach 'tree' => $tree, 44600ef1d3aSGreg Roach ]); 44700ef1d3aSGreg Roach } 44800ef1d3aSGreg Roach } 44900ef1d3aSGreg Roach } 45000ef1d3aSGreg Roach } ?> 45100ef1d3aSGreg Roach </div> 45200ef1d3aSGreg Roach <?php 45300ef1d3aSGreg Roach 45400ef1d3aSGreg Roach $html = ob_get_clean(); 45500ef1d3aSGreg Roach 45600ef1d3aSGreg Roach return $this->viewResponse('modules/individual-list/page', [ 45700ef1d3aSGreg Roach 'content' => $html, 45800ef1d3aSGreg Roach 'title' => $title, 45900ef1d3aSGreg Roach 'tree' => $tree, 46000ef1d3aSGreg Roach ]); 46100ef1d3aSGreg Roach } 46200ef1d3aSGreg Roach 46300ef1d3aSGreg Roach /** 46400ef1d3aSGreg Roach * Some initial letters have a special meaning 46500ef1d3aSGreg Roach */ 46600ef1d3aSGreg Roach protected function displayGivenNameInitial(string $initial): string 46700ef1d3aSGreg Roach { 46800ef1d3aSGreg Roach if ($initial === '@') { 46900ef1d3aSGreg Roach return I18N::translateContext('Unknown given name', '…'); 47000ef1d3aSGreg Roach } 47100ef1d3aSGreg Roach 47200ef1d3aSGreg Roach return e($initial); 47300ef1d3aSGreg Roach } 47400ef1d3aSGreg Roach 47500ef1d3aSGreg Roach /** 47600ef1d3aSGreg Roach * Some initial letters have a special meaning 47700ef1d3aSGreg Roach */ 47800ef1d3aSGreg Roach protected function displaySurnameInitial(string $initial): string 47900ef1d3aSGreg Roach { 48000ef1d3aSGreg Roach if ($initial === '@') { 48100ef1d3aSGreg Roach return I18N::translateContext('Unknown surname', '…'); 48200ef1d3aSGreg Roach } 48300ef1d3aSGreg Roach 48400ef1d3aSGreg Roach if ($initial === ',') { 48500ef1d3aSGreg Roach return I18N::translate('No surname'); 48600ef1d3aSGreg Roach } 48700ef1d3aSGreg Roach 48800ef1d3aSGreg Roach return e($initial); 48900ef1d3aSGreg Roach } 49000ef1d3aSGreg Roach 49100ef1d3aSGreg Roach /** 49200ef1d3aSGreg Roach * Restrict a query to individuals that are a spouse in a family record. 49300ef1d3aSGreg Roach */ 49400ef1d3aSGreg Roach protected function whereFamily(bool $fams, Builder $query): void 49500ef1d3aSGreg Roach { 49600ef1d3aSGreg Roach if ($fams) { 49700ef1d3aSGreg Roach $query->join('link', static function (JoinClause $join): void { 49800ef1d3aSGreg Roach $join 49900ef1d3aSGreg Roach ->on('l_from', '=', 'n_id') 50000ef1d3aSGreg Roach ->on('l_file', '=', 'n_file') 50100ef1d3aSGreg Roach ->where('l_type', '=', 'FAMS'); 50200ef1d3aSGreg Roach }); 50300ef1d3aSGreg Roach } 50400ef1d3aSGreg Roach } 50500ef1d3aSGreg Roach 50600ef1d3aSGreg Roach /** 50700ef1d3aSGreg Roach * Restrict a query to include/exclude married names. 50800ef1d3aSGreg Roach */ 50900ef1d3aSGreg Roach protected function whereMarriedName(bool $marnm, Builder $query): void 51000ef1d3aSGreg Roach { 51100ef1d3aSGreg Roach if (!$marnm) { 51200ef1d3aSGreg Roach $query->where('n_type', '<>', '_MARNM'); 51300ef1d3aSGreg Roach } 51400ef1d3aSGreg Roach } 51500ef1d3aSGreg Roach 51600ef1d3aSGreg Roach /** 51700ef1d3aSGreg Roach * Get a count of individuals with each initial letter 51800ef1d3aSGreg Roach * 51900ef1d3aSGreg Roach * @param array<string> $surns if set, only consider people with this surname 52000ef1d3aSGreg Roach * @param bool $marnm if set, include married names 52100ef1d3aSGreg Roach * @param bool $fams if set, only consider individuals with FAMS records 52200ef1d3aSGreg Roach * 52300ef1d3aSGreg Roach * @return array<int> 52400ef1d3aSGreg Roach */ 52500ef1d3aSGreg Roach public function givenNameInitials(Tree $tree, array $surns, bool $marnm, bool $fams): array 52600ef1d3aSGreg Roach { 52700ef1d3aSGreg Roach $initials = []; 52800ef1d3aSGreg Roach 52900ef1d3aSGreg Roach // Ensure our own language comes before others. 53000ef1d3aSGreg Roach foreach (I18N::language()->alphabet() as $initial) { 53100ef1d3aSGreg Roach $initials[$initial] = 0; 53200ef1d3aSGreg Roach } 53300ef1d3aSGreg Roach 53400ef1d3aSGreg Roach $query = DB::table('name') 53500ef1d3aSGreg Roach ->where('n_file', '=', $tree->id()); 53600ef1d3aSGreg Roach 53700ef1d3aSGreg Roach $this->whereFamily($fams, $query); 53800ef1d3aSGreg Roach $this->whereMarriedName($marnm, $query); 53900ef1d3aSGreg Roach 54000ef1d3aSGreg Roach if ($surns !== []) { 54100ef1d3aSGreg Roach $query->whereIn('n_surn', $surns); 54200ef1d3aSGreg Roach } 54300ef1d3aSGreg Roach 54400ef1d3aSGreg Roach $query 54500ef1d3aSGreg Roach ->select([DB::binaryColumn('n_givn', 'n_givn'), new Expression('COUNT(*) AS count')]) 54600ef1d3aSGreg Roach ->groupBy([DB::binaryColumn('n_givn')]); 54700ef1d3aSGreg Roach 54800ef1d3aSGreg Roach foreach ($query->get() as $row) { 54900ef1d3aSGreg Roach $initial = I18N::strtoupper(I18N::language()->initialLetter($row->n_givn)); 55000ef1d3aSGreg Roach $initials[$initial] ??= 0; 55100ef1d3aSGreg Roach $initials[$initial] += (int) $row->count; 55200ef1d3aSGreg Roach } 55300ef1d3aSGreg Roach 55400ef1d3aSGreg Roach $count_unknown = $initials['@'] ?? 0; 55500ef1d3aSGreg Roach 55600ef1d3aSGreg Roach if ($count_unknown > 0) { 55700ef1d3aSGreg Roach unset($initials['@']); 55800ef1d3aSGreg Roach $initials['@'] = $count_unknown; 55900ef1d3aSGreg Roach } 56000ef1d3aSGreg Roach 56100ef1d3aSGreg Roach return $initials; 56200ef1d3aSGreg Roach } 56300ef1d3aSGreg Roach 56400ef1d3aSGreg Roach /** 56500ef1d3aSGreg Roach * @return array<object{n_surn:string,n_surname:string,total:int}> 56600ef1d3aSGreg Roach */ 56700ef1d3aSGreg Roach private function surnameData(Tree $tree, bool $marnm, bool $fams): array 56800ef1d3aSGreg Roach { 56900ef1d3aSGreg Roach $query = DB::table('name') 57000ef1d3aSGreg Roach ->where('n_file', '=', $tree->id()) 57100ef1d3aSGreg Roach ->whereNotNull('n_surn') // Filters old records for sources, repositories, etc. 57200ef1d3aSGreg Roach ->whereNotNull('n_surname') 57300ef1d3aSGreg Roach ->select([ 57400ef1d3aSGreg Roach DB::binaryColumn('n_surn', 'n_surn'), 57500ef1d3aSGreg Roach DB::binaryColumn('n_surname', 'n_surname'), 57600ef1d3aSGreg Roach new Expression('COUNT(*) AS total'), 57700ef1d3aSGreg Roach ]); 57800ef1d3aSGreg Roach 57900ef1d3aSGreg Roach $this->whereFamily($fams, $query); 58000ef1d3aSGreg Roach $this->whereMarriedName($marnm, $query); 58100ef1d3aSGreg Roach 58200ef1d3aSGreg Roach $query->groupBy([ 58300ef1d3aSGreg Roach DB::binaryColumn('n_surn'), 58400ef1d3aSGreg Roach DB::binaryColumn('n_surname'), 58500ef1d3aSGreg Roach ]); 58600ef1d3aSGreg Roach 58700ef1d3aSGreg Roach return $query 58800ef1d3aSGreg Roach ->get() 58900ef1d3aSGreg Roach ->map(static fn (object $x): object => (object) ['n_surn' => $x->n_surn, 'n_surname' => $x->n_surname, 'total' => (int) $x->total]) 59000ef1d3aSGreg Roach ->all(); 59100ef1d3aSGreg Roach } 59200ef1d3aSGreg Roach 59300ef1d3aSGreg Roach /** 59400ef1d3aSGreg Roach * Group n_surn values, based on collation rules for the current language. 59500ef1d3aSGreg Roach * We need them to find the individuals with this n_surn. 59600ef1d3aSGreg Roach * 59700ef1d3aSGreg Roach * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data 59800ef1d3aSGreg Roach * 59900ef1d3aSGreg Roach * @return array<array<int,string>> 60000ef1d3aSGreg Roach */ 60100ef1d3aSGreg Roach protected function allSurns(array $surname_data): array 60200ef1d3aSGreg Roach { 60300ef1d3aSGreg Roach $list = []; 60400ef1d3aSGreg Roach 60500ef1d3aSGreg Roach foreach ($surname_data as $row) { 60600ef1d3aSGreg Roach $normalized = I18N::strtoupper(I18N::language()->normalize($row->n_surn)); 60700ef1d3aSGreg Roach $list[$normalized][] = $row->n_surn; 60800ef1d3aSGreg Roach } 60900ef1d3aSGreg Roach 61000ef1d3aSGreg Roach uksort($list, I18N::comparator()); 61100ef1d3aSGreg Roach 61200ef1d3aSGreg Roach return $list; 61300ef1d3aSGreg Roach } 61400ef1d3aSGreg Roach 61500ef1d3aSGreg Roach /** 61600ef1d3aSGreg Roach * Group n_surname values, based on collation rules for each n_surn. 61700ef1d3aSGreg Roach * We need them to show counts of individuals with each surname. 61800ef1d3aSGreg Roach * 61900ef1d3aSGreg Roach * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data 62000ef1d3aSGreg Roach * 62100ef1d3aSGreg Roach * @return array<array<int>> 62200ef1d3aSGreg Roach */ 62300ef1d3aSGreg Roach protected function allSurnames(array $surname_data): array 62400ef1d3aSGreg Roach { 62500ef1d3aSGreg Roach $list = []; 62600ef1d3aSGreg Roach 62700ef1d3aSGreg Roach foreach ($surname_data as $row) { 62800ef1d3aSGreg Roach $n_surn = $row->n_surn === '' ? $row->n_surname : $row->n_surn; 62900ef1d3aSGreg Roach $n_surn = I18N::strtoupper(I18N::language()->normalize($n_surn)); 63000ef1d3aSGreg Roach 63100ef1d3aSGreg Roach $list[$n_surn][$row->n_surname] ??= 0; 63200ef1d3aSGreg Roach $list[$n_surn][$row->n_surname] += $row->total; 63300ef1d3aSGreg Roach } 63400ef1d3aSGreg Roach 63500ef1d3aSGreg Roach uksort($list, I18N::comparator()); 63600ef1d3aSGreg Roach 63700ef1d3aSGreg Roach return $list; 63800ef1d3aSGreg Roach } 63900ef1d3aSGreg Roach 64000ef1d3aSGreg Roach /** 64100ef1d3aSGreg Roach * Extract initial letters and counts for all surnames. 64200ef1d3aSGreg Roach * 64300ef1d3aSGreg Roach * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data 64400ef1d3aSGreg Roach * 64500ef1d3aSGreg Roach * @return array<int> 64600ef1d3aSGreg Roach */ 64700ef1d3aSGreg Roach protected function surnameInitials(array $surname_data): array 64800ef1d3aSGreg Roach { 64900ef1d3aSGreg Roach $initials = []; 65000ef1d3aSGreg Roach 65100ef1d3aSGreg Roach // Ensure our own language comes before others. 65200ef1d3aSGreg Roach foreach (I18N::language()->alphabet() as $initial) { 65300ef1d3aSGreg Roach $initials[$initial] = 0; 65400ef1d3aSGreg Roach } 65500ef1d3aSGreg Roach 65600ef1d3aSGreg Roach foreach ($surname_data as $row) { 65700ef1d3aSGreg Roach $initial = I18N::language()->initialLetter(I18N::strtoupper($row->n_surn)); 65800ef1d3aSGreg Roach $initial = I18N::language()->normalize($initial); 65900ef1d3aSGreg Roach 66000ef1d3aSGreg Roach $initials[$initial] ??= 0; 66100ef1d3aSGreg Roach $initials[$initial] += $row->total; 66200ef1d3aSGreg Roach } 66300ef1d3aSGreg Roach 66400ef1d3aSGreg Roach // Move specials to the end 66500ef1d3aSGreg Roach $count_none = $initials[''] ?? 0; 66600ef1d3aSGreg Roach 66700ef1d3aSGreg Roach if ($count_none > 0) { 66800ef1d3aSGreg Roach unset($initials['']); 66900ef1d3aSGreg Roach $initials[','] = $count_none; 67000ef1d3aSGreg Roach } 67100ef1d3aSGreg Roach 67200ef1d3aSGreg Roach $count_unknown = $initials['@'] ?? 0; 67300ef1d3aSGreg Roach 67400ef1d3aSGreg Roach if ($count_unknown > 0) { 67500ef1d3aSGreg Roach unset($initials['@']); 67600ef1d3aSGreg Roach $initials['@'] = $count_unknown; 67700ef1d3aSGreg Roach } 67800ef1d3aSGreg Roach 67900ef1d3aSGreg Roach return $initials; 68000ef1d3aSGreg Roach } 68100ef1d3aSGreg Roach 68200ef1d3aSGreg Roach /** 68300ef1d3aSGreg Roach * Fetch a list of individuals with specified names 68400ef1d3aSGreg Roach * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@" 68500ef1d3aSGreg Roach * To search for names with no surnames, use $salpha="," 68600ef1d3aSGreg Roach * 68700ef1d3aSGreg Roach * @param array<string> $surns_to_show if set, only fetch people with this n_surn 68800ef1d3aSGreg Roach * @param string $galpha if set, only fetch given names starting with this letter 68900ef1d3aSGreg Roach * @param bool $marnm if set, include married names 69000ef1d3aSGreg Roach * @param bool $fams if set, only fetch individuals with FAMS records 69100ef1d3aSGreg Roach * 69200ef1d3aSGreg Roach * @return Collection<int,Individual> 69300ef1d3aSGreg Roach */ 69400ef1d3aSGreg Roach protected function individuals(Tree $tree, array $surns_to_show, string $galpha, bool $marnm, bool $fams): Collection 69500ef1d3aSGreg Roach { 69600ef1d3aSGreg Roach $query = DB::table('individuals') 69700ef1d3aSGreg Roach ->join('name', static function (JoinClause $join): void { 69800ef1d3aSGreg Roach $join 69900ef1d3aSGreg Roach ->on('n_id', '=', 'i_id') 70000ef1d3aSGreg Roach ->on('n_file', '=', 'i_file'); 70100ef1d3aSGreg Roach }) 70200ef1d3aSGreg Roach ->where('i_file', '=', $tree->id()) 70300ef1d3aSGreg Roach ->select(['i_id AS xref', 'i_gedcom AS gedcom', 'n_givn', 'n_surn']); 70400ef1d3aSGreg Roach 70500ef1d3aSGreg Roach $this->whereFamily($fams, $query); 70600ef1d3aSGreg Roach $this->whereMarriedName($marnm, $query); 70700ef1d3aSGreg Roach 70800ef1d3aSGreg Roach if ($surns_to_show === []) { 70900ef1d3aSGreg Roach $query->whereNotIn('n_surn', ['', '@N.N.']); 71000ef1d3aSGreg Roach } else { 71100ef1d3aSGreg Roach $query->whereIn(DB::binaryColumn('n_surn'), $surns_to_show); 71200ef1d3aSGreg Roach } 71300ef1d3aSGreg Roach 71400ef1d3aSGreg Roach $individuals = new Collection(); 71500ef1d3aSGreg Roach 71600ef1d3aSGreg Roach foreach ($query->get() as $row) { 71700ef1d3aSGreg Roach $individual = Registry::individualFactory()->make($row->xref, $tree, $row->gedcom); 71800ef1d3aSGreg Roach assert($individual instanceof Individual); 71900ef1d3aSGreg Roach 72000ef1d3aSGreg Roach // The name from the database may be private - check the filtered list... 72100ef1d3aSGreg Roach foreach ($individual->getAllNames() as $n => $name) { 72200ef1d3aSGreg Roach if ($name['givn'] === $row->n_givn && $name['surn'] === $row->n_surn) { 72300ef1d3aSGreg Roach if ($galpha === '' || I18N::strtoupper(I18N::language()->initialLetter($row->n_givn)) === $galpha) { 72400ef1d3aSGreg Roach $individual->setPrimaryName($n); 72500ef1d3aSGreg Roach // We need to clone $individual, as we may have multiple references to the 72600ef1d3aSGreg Roach // same individual in this list, and the "primary name" would otherwise 72700ef1d3aSGreg Roach // be shared amongst all of them. 72800ef1d3aSGreg Roach $individuals->push(clone $individual); 72900ef1d3aSGreg Roach break; 73000ef1d3aSGreg Roach } 73100ef1d3aSGreg Roach } 73200ef1d3aSGreg Roach } 73300ef1d3aSGreg Roach } 73400ef1d3aSGreg Roach 73500ef1d3aSGreg Roach return $individuals; 73600ef1d3aSGreg Roach } 73700ef1d3aSGreg Roach 73800ef1d3aSGreg Roach /** 73900ef1d3aSGreg Roach * Fetch a list of families with specified names 74000ef1d3aSGreg Roach * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@" 74100ef1d3aSGreg Roach * To search for names with no surnames, use $salpha="," 74200ef1d3aSGreg Roach * 74300ef1d3aSGreg Roach * @param array<string> $surnames if set, only fetch people with this n_surname 74400ef1d3aSGreg Roach * @param string $galpha if set, only fetch given names starting with this letter 74500ef1d3aSGreg Roach * @param bool $marnm if set, include married names 74600ef1d3aSGreg Roach * 74700ef1d3aSGreg Roach * @return Collection<int,Family> 74800ef1d3aSGreg Roach */ 74900ef1d3aSGreg Roach protected function families(Tree $tree, array $surnames, string $galpha, bool $marnm): Collection 75000ef1d3aSGreg Roach { 75100ef1d3aSGreg Roach $families = new Collection(); 75200ef1d3aSGreg Roach 75300ef1d3aSGreg Roach foreach ($this->individuals($tree, $surnames, $galpha, $marnm, true) as $indi) { 75400ef1d3aSGreg Roach foreach ($indi->spouseFamilies() as $family) { 75500ef1d3aSGreg Roach $families->push($family); 75600ef1d3aSGreg Roach } 75700ef1d3aSGreg Roach } 75800ef1d3aSGreg Roach 75900ef1d3aSGreg Roach return $families->unique(); 76000ef1d3aSGreg Roach } 76100ef1d3aSGreg Roach} 762