xref: /webtrees/app/Module/AbstractIndividualListModule.php (revision 01b92fd6b02917bce4b544b1295f7954fc92e3ce)
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">
311c9eec602SGreg 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') {
3852ed4bd58SFranz Frese                            echo '<h2 class="wt-page-title">', I18N::translate('Given names'), '</h2>';
38600ef1d3aSGreg Roach                            // Don't show the list until we have some filter criteria
38700ef1d3aSGreg Roach                            $show = $falpha !== '' || $show_all_firstnames === 'yes' ? 'indi' : 'none';
38800ef1d3aSGreg Roach                            echo '<ul class="d-flex flex-wrap list-unstyled justify-content-center wt-initials-list wt-initials-list-given-names">';
38900ef1d3aSGreg Roach                            foreach ($givn_initials as $givn_initial => $given_count) {
39000ef1d3aSGreg Roach                                echo '<li class="wt-initials-list-item d-flex">';
39100ef1d3aSGreg Roach                                if ($given_count > 0) {
39200ef1d3aSGreg Roach                                    if ($show === 'indi' && $givn_initial === $falpha && $show_all_firstnames !== 'yes') {
39300ef1d3aSGreg 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>';
39400ef1d3aSGreg Roach                                    } else {
39500ef1d3aSGreg 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>';
39600ef1d3aSGreg Roach                                    }
39700ef1d3aSGreg Roach                                } else {
39800ef1d3aSGreg Roach                                    echo '<span class="wt-initial px-1 text-muted">' . $this->displayGivenNameInitial((string) $givn_initial) . '</span>';
39900ef1d3aSGreg Roach                                }
40000ef1d3aSGreg Roach                                echo '</li>';
40100ef1d3aSGreg Roach                            }
40200ef1d3aSGreg Roach                            // Search spiders don't get the "show all" option as the other links give them everything.
40300ef1d3aSGreg Roach                            if (Session::has('initiated')) {
40400ef1d3aSGreg Roach                                echo '<li class="wt-initials-list-item d-flex">';
40500ef1d3aSGreg Roach                                if ($show_all_firstnames === 'yes') {
40600ef1d3aSGreg Roach                                    echo '<span class="wt-initial px-1 active">' . I18N::translate('All') . '</span>';
40700ef1d3aSGreg Roach                                } else {
40800ef1d3aSGreg 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>';
40900ef1d3aSGreg Roach                                }
41000ef1d3aSGreg Roach                                echo '</li>';
41100ef1d3aSGreg Roach                            }
41200ef1d3aSGreg Roach                            echo '</ul>';
41300ef1d3aSGreg Roach                        }
41400ef1d3aSGreg Roach                    }
41500ef1d3aSGreg Roach                    if ($show === 'indi') {
41600ef1d3aSGreg Roach                        if ($alpha === '@') {
41700ef1d3aSGreg Roach                            $surns_to_show = ['@N.N.'];
41800ef1d3aSGreg Roach                        } elseif ($alpha === ',') {
41900ef1d3aSGreg Roach                            $surns_to_show = [''];
42000ef1d3aSGreg Roach                        } elseif ($surname !== '') {
42100ef1d3aSGreg Roach                            $surns_to_show = $all_surns[$surname];
42200ef1d3aSGreg Roach                        } elseif ($alpha !== '') {
42300ef1d3aSGreg Roach                            $tmp = array_filter(
42400ef1d3aSGreg Roach                                $all_surns,
42500ef1d3aSGreg Roach                                static fn (string $x): bool => I18N::language()->initialLetter($x) === $alpha,
42600ef1d3aSGreg Roach                                ARRAY_FILTER_USE_KEY
42700ef1d3aSGreg Roach                            );
42800ef1d3aSGreg Roach
42900ef1d3aSGreg Roach                            $surns_to_show = array_merge(...array_values($tmp));
43000ef1d3aSGreg Roach                        } else {
43100ef1d3aSGreg Roach                            $surns_to_show = [];
43200ef1d3aSGreg Roach                        }
43300ef1d3aSGreg Roach
43400ef1d3aSGreg Roach                        if ($this->showFamilies()) {
43500ef1d3aSGreg Roach                            echo view('lists/families-table', [
43600ef1d3aSGreg Roach                                'families' => $this->families($tree, $surns_to_show, $falpha, $show_marnm === 'yes'),
43700ef1d3aSGreg Roach                                'tree'     => $tree,
43800ef1d3aSGreg Roach                            ]);
43900ef1d3aSGreg Roach                        } else {
44000ef1d3aSGreg Roach                            echo view('lists/individuals-table', [
44100ef1d3aSGreg Roach                                'individuals' => $this->individuals($tree, $surns_to_show, $falpha, $show_marnm === 'yes', false),
44200ef1d3aSGreg Roach                                'sosa'        => false,
44300ef1d3aSGreg Roach                                'tree'        => $tree,
44400ef1d3aSGreg Roach                            ]);
44500ef1d3aSGreg Roach                        }
44600ef1d3aSGreg Roach                    }
44700ef1d3aSGreg Roach                }
44800ef1d3aSGreg Roach            } ?>
44900ef1d3aSGreg Roach        </div>
45000ef1d3aSGreg Roach        <?php
45100ef1d3aSGreg Roach
45200ef1d3aSGreg Roach        $html = ob_get_clean();
45300ef1d3aSGreg Roach
45400ef1d3aSGreg Roach        return $this->viewResponse('modules/individual-list/page', [
45500ef1d3aSGreg Roach            'content' => $html,
45600ef1d3aSGreg Roach            'title'   => $title,
45700ef1d3aSGreg Roach            'tree'    => $tree,
45800ef1d3aSGreg Roach        ]);
45900ef1d3aSGreg Roach    }
46000ef1d3aSGreg Roach
46100ef1d3aSGreg Roach    /**
46200ef1d3aSGreg Roach     * Some initial letters have a special meaning
46300ef1d3aSGreg Roach     */
46400ef1d3aSGreg Roach    protected function displayGivenNameInitial(string $initial): string
46500ef1d3aSGreg Roach    {
46600ef1d3aSGreg Roach        if ($initial === '@') {
46700ef1d3aSGreg Roach            return I18N::translateContext('Unknown given name', '…');
46800ef1d3aSGreg Roach        }
46900ef1d3aSGreg Roach
47000ef1d3aSGreg Roach        return e($initial);
47100ef1d3aSGreg Roach    }
47200ef1d3aSGreg Roach
47300ef1d3aSGreg Roach    /**
47400ef1d3aSGreg Roach     * Some initial letters have a special meaning
47500ef1d3aSGreg Roach     */
47600ef1d3aSGreg Roach    protected function displaySurnameInitial(string $initial): string
47700ef1d3aSGreg Roach    {
47800ef1d3aSGreg Roach        if ($initial === '@') {
47900ef1d3aSGreg Roach            return I18N::translateContext('Unknown surname', '…');
48000ef1d3aSGreg Roach        }
48100ef1d3aSGreg Roach
48200ef1d3aSGreg Roach        if ($initial === ',') {
48300ef1d3aSGreg Roach            return I18N::translate('No surname');
48400ef1d3aSGreg Roach        }
48500ef1d3aSGreg Roach
48600ef1d3aSGreg Roach        return e($initial);
48700ef1d3aSGreg Roach    }
48800ef1d3aSGreg Roach
48900ef1d3aSGreg Roach    /**
49000ef1d3aSGreg Roach     * Restrict a query to individuals that are a spouse in a family record.
49100ef1d3aSGreg Roach     */
49200ef1d3aSGreg Roach    protected function whereFamily(bool $fams, Builder $query): void
49300ef1d3aSGreg Roach    {
49400ef1d3aSGreg Roach        if ($fams) {
49500ef1d3aSGreg Roach            $query->join('link', static function (JoinClause $join): void {
49600ef1d3aSGreg Roach                $join
49700ef1d3aSGreg Roach                    ->on('l_from', '=', 'n_id')
49800ef1d3aSGreg Roach                    ->on('l_file', '=', 'n_file')
49900ef1d3aSGreg Roach                    ->where('l_type', '=', 'FAMS');
50000ef1d3aSGreg Roach            });
50100ef1d3aSGreg Roach        }
50200ef1d3aSGreg Roach    }
50300ef1d3aSGreg Roach
50400ef1d3aSGreg Roach    /**
50500ef1d3aSGreg Roach     * Restrict a query to include/exclude married names.
50600ef1d3aSGreg Roach     */
50700ef1d3aSGreg Roach    protected function whereMarriedName(bool $marnm, Builder $query): void
50800ef1d3aSGreg Roach    {
50900ef1d3aSGreg Roach        if (!$marnm) {
51000ef1d3aSGreg Roach            $query->where('n_type', '<>', '_MARNM');
51100ef1d3aSGreg Roach        }
51200ef1d3aSGreg Roach    }
51300ef1d3aSGreg Roach
51400ef1d3aSGreg Roach    /**
51500ef1d3aSGreg Roach     * Get a count of individuals with each initial letter
51600ef1d3aSGreg Roach     *
51700ef1d3aSGreg Roach     * @param array<string> $surns if set, only consider people with this surname
51800ef1d3aSGreg Roach     * @param bool          $marnm if set, include married names
51900ef1d3aSGreg Roach     * @param bool          $fams  if set, only consider individuals with FAMS records
52000ef1d3aSGreg Roach     *
52100ef1d3aSGreg Roach     * @return array<int>
52200ef1d3aSGreg Roach     */
52300ef1d3aSGreg Roach    public function givenNameInitials(Tree $tree, array $surns, bool $marnm, bool $fams): array
52400ef1d3aSGreg Roach    {
52500ef1d3aSGreg Roach        $initials = [];
52600ef1d3aSGreg Roach
52700ef1d3aSGreg Roach        // Ensure our own language comes before others.
52800ef1d3aSGreg Roach        foreach (I18N::language()->alphabet() as $initial) {
52900ef1d3aSGreg Roach            $initials[$initial] = 0;
53000ef1d3aSGreg Roach        }
53100ef1d3aSGreg Roach
53200ef1d3aSGreg Roach        $query = DB::table('name')
53300ef1d3aSGreg Roach            ->where('n_file', '=', $tree->id());
53400ef1d3aSGreg Roach
53500ef1d3aSGreg Roach        $this->whereFamily($fams, $query);
53600ef1d3aSGreg Roach        $this->whereMarriedName($marnm, $query);
53700ef1d3aSGreg Roach
53800ef1d3aSGreg Roach        if ($surns !== []) {
53900ef1d3aSGreg Roach            $query->whereIn('n_surn', $surns);
54000ef1d3aSGreg Roach        }
54100ef1d3aSGreg Roach
54200ef1d3aSGreg Roach        $query
54300ef1d3aSGreg Roach            ->select([DB::binaryColumn('n_givn', 'n_givn'), new Expression('COUNT(*) AS count')])
54400ef1d3aSGreg Roach            ->groupBy([DB::binaryColumn('n_givn')]);
54500ef1d3aSGreg Roach
54600ef1d3aSGreg Roach        foreach ($query->get() as $row) {
547d287832dSGreg Roach            $initial = I18N::language()->initialLetter(I18N::language()->normalize(I18N::strtoupper($row->n_givn)));
548d287832dSGreg Roach
54900ef1d3aSGreg Roach            $initials[$initial] ??= 0;
55000ef1d3aSGreg Roach            $initials[$initial] += (int) $row->count;
55100ef1d3aSGreg Roach        }
55200ef1d3aSGreg Roach
55300ef1d3aSGreg Roach        $count_unknown = $initials['@'] ?? 0;
55400ef1d3aSGreg Roach
55500ef1d3aSGreg Roach        if ($count_unknown > 0) {
55600ef1d3aSGreg Roach            unset($initials['@']);
55700ef1d3aSGreg Roach            $initials['@'] = $count_unknown;
55800ef1d3aSGreg Roach        }
55900ef1d3aSGreg Roach
56000ef1d3aSGreg Roach        return $initials;
56100ef1d3aSGreg Roach    }
56200ef1d3aSGreg Roach
56300ef1d3aSGreg Roach    /**
56400ef1d3aSGreg Roach     * @return array<object{n_surn:string,n_surname:string,total:int}>
56500ef1d3aSGreg Roach     */
56600ef1d3aSGreg Roach    private function surnameData(Tree $tree, bool $marnm, bool $fams): array
56700ef1d3aSGreg Roach    {
56800ef1d3aSGreg Roach        $query = DB::table('name')
56900ef1d3aSGreg Roach            ->where('n_file', '=', $tree->id())
57000ef1d3aSGreg Roach            ->whereNotNull('n_surn') // Filters old records for sources, repositories, etc.
57100ef1d3aSGreg Roach            ->whereNotNull('n_surname')
57200ef1d3aSGreg Roach            ->select([
57300ef1d3aSGreg Roach                DB::binaryColumn('n_surn', 'n_surn'),
57400ef1d3aSGreg Roach                DB::binaryColumn('n_surname', 'n_surname'),
57500ef1d3aSGreg Roach                new Expression('COUNT(*) AS total'),
57600ef1d3aSGreg Roach            ]);
57700ef1d3aSGreg Roach
57800ef1d3aSGreg Roach        $this->whereFamily($fams, $query);
57900ef1d3aSGreg Roach        $this->whereMarriedName($marnm, $query);
58000ef1d3aSGreg Roach
58100ef1d3aSGreg Roach        $query->groupBy([
58200ef1d3aSGreg Roach            DB::binaryColumn('n_surn'),
58300ef1d3aSGreg Roach            DB::binaryColumn('n_surname'),
58400ef1d3aSGreg Roach        ]);
58500ef1d3aSGreg Roach
58600ef1d3aSGreg Roach        return $query
58700ef1d3aSGreg Roach            ->get()
58800ef1d3aSGreg Roach            ->map(static fn (object $x): object => (object) ['n_surn' => $x->n_surn, 'n_surname' => $x->n_surname, 'total' => (int) $x->total])
58900ef1d3aSGreg Roach            ->all();
59000ef1d3aSGreg Roach    }
59100ef1d3aSGreg Roach
59200ef1d3aSGreg Roach    /**
59300ef1d3aSGreg Roach     * Group n_surn values, based on collation rules for the current language.
59400ef1d3aSGreg Roach     * We need them to find the individuals with this n_surn.
59500ef1d3aSGreg Roach     *
59600ef1d3aSGreg Roach     * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data
59700ef1d3aSGreg Roach     *
59800ef1d3aSGreg Roach     * @return array<array<int,string>>
59900ef1d3aSGreg Roach     */
60000ef1d3aSGreg Roach    protected function allSurns(array $surname_data): array
60100ef1d3aSGreg Roach    {
60200ef1d3aSGreg Roach        $list = [];
60300ef1d3aSGreg Roach
60400ef1d3aSGreg Roach        foreach ($surname_data as $row) {
60500ef1d3aSGreg Roach            $normalized = I18N::strtoupper(I18N::language()->normalize($row->n_surn));
60600ef1d3aSGreg Roach            $list[$normalized][] = $row->n_surn;
60700ef1d3aSGreg Roach        }
60800ef1d3aSGreg Roach
60900ef1d3aSGreg Roach        uksort($list, I18N::comparator());
61000ef1d3aSGreg Roach
61100ef1d3aSGreg Roach        return $list;
61200ef1d3aSGreg Roach    }
61300ef1d3aSGreg Roach
61400ef1d3aSGreg Roach    /**
61500ef1d3aSGreg Roach     * Group n_surname values, based on collation rules for each n_surn.
61600ef1d3aSGreg Roach     * We need them to show counts of individuals with each surname.
61700ef1d3aSGreg Roach     *
61800ef1d3aSGreg Roach     * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data
61900ef1d3aSGreg Roach     *
62000ef1d3aSGreg Roach     * @return array<array<int>>
62100ef1d3aSGreg Roach     */
62200ef1d3aSGreg Roach    protected function allSurnames(array $surname_data): array
62300ef1d3aSGreg Roach    {
62400ef1d3aSGreg Roach        $list = [];
62500ef1d3aSGreg Roach
62600ef1d3aSGreg Roach        foreach ($surname_data as $row) {
62700ef1d3aSGreg Roach            $n_surn = $row->n_surn === '' ? $row->n_surname : $row->n_surn;
62800ef1d3aSGreg Roach            $n_surn = I18N::strtoupper(I18N::language()->normalize($n_surn));
62900ef1d3aSGreg Roach
63000ef1d3aSGreg Roach            $list[$n_surn][$row->n_surname] ??= 0;
63100ef1d3aSGreg Roach            $list[$n_surn][$row->n_surname] += $row->total;
63200ef1d3aSGreg Roach        }
63300ef1d3aSGreg Roach
63400ef1d3aSGreg Roach        uksort($list, I18N::comparator());
63500ef1d3aSGreg Roach
63600ef1d3aSGreg Roach        return $list;
63700ef1d3aSGreg Roach    }
63800ef1d3aSGreg Roach
63900ef1d3aSGreg Roach    /**
64000ef1d3aSGreg Roach     * Extract initial letters and counts for all surnames.
64100ef1d3aSGreg Roach     *
64200ef1d3aSGreg Roach     * @param array<object{n_surn:string,n_surname:string,total:int}> $surname_data
64300ef1d3aSGreg Roach     *
64400ef1d3aSGreg Roach     * @return array<int>
64500ef1d3aSGreg Roach     */
64600ef1d3aSGreg Roach    protected function surnameInitials(array $surname_data): array
64700ef1d3aSGreg Roach    {
64800ef1d3aSGreg Roach        $initials = [];
64900ef1d3aSGreg Roach
65000ef1d3aSGreg Roach        // Ensure our own language comes before others.
65100ef1d3aSGreg Roach        foreach (I18N::language()->alphabet() as $initial) {
65200ef1d3aSGreg Roach            $initials[$initial] = 0;
65300ef1d3aSGreg Roach        }
65400ef1d3aSGreg Roach
65500ef1d3aSGreg Roach        foreach ($surname_data as $row) {
656f62cbbb1SGreg Roach            $initial = I18N::language()->initialLetter(I18N::language()->normalize(I18N::strtoupper($row->n_surn)));
65700ef1d3aSGreg Roach
65800ef1d3aSGreg Roach            $initials[$initial] ??= 0;
65900ef1d3aSGreg Roach            $initials[$initial] += $row->total;
66000ef1d3aSGreg Roach        }
66100ef1d3aSGreg Roach
66200ef1d3aSGreg Roach        // Move specials to the end
66300ef1d3aSGreg Roach        $count_none = $initials[''] ?? 0;
66400ef1d3aSGreg Roach
66500ef1d3aSGreg Roach        if ($count_none > 0) {
66600ef1d3aSGreg Roach            unset($initials['']);
66700ef1d3aSGreg Roach            $initials[','] = $count_none;
66800ef1d3aSGreg Roach        }
66900ef1d3aSGreg Roach
67000ef1d3aSGreg Roach        $count_unknown = $initials['@'] ?? 0;
67100ef1d3aSGreg Roach
67200ef1d3aSGreg Roach        if ($count_unknown > 0) {
67300ef1d3aSGreg Roach            unset($initials['@']);
67400ef1d3aSGreg Roach            $initials['@'] = $count_unknown;
67500ef1d3aSGreg Roach        }
67600ef1d3aSGreg Roach
67700ef1d3aSGreg Roach        return $initials;
67800ef1d3aSGreg Roach    }
67900ef1d3aSGreg Roach
68000ef1d3aSGreg Roach    /**
68100ef1d3aSGreg Roach     * Fetch a list of individuals with specified names
68200ef1d3aSGreg Roach     * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
68300ef1d3aSGreg Roach     * To search for names with no surnames, use $salpha=","
68400ef1d3aSGreg Roach     *
68500ef1d3aSGreg Roach     * @param array<string> $surns_to_show if set, only fetch people with this n_surn
68600ef1d3aSGreg Roach     * @param string        $galpha        if set, only fetch given names starting with this letter
68700ef1d3aSGreg Roach     * @param bool          $marnm         if set, include married names
68800ef1d3aSGreg Roach     * @param bool          $fams          if set, only fetch individuals with FAMS records
68900ef1d3aSGreg Roach     *
69000ef1d3aSGreg Roach     * @return Collection<int,Individual>
69100ef1d3aSGreg Roach     */
69200ef1d3aSGreg Roach    protected function individuals(Tree $tree, array $surns_to_show, string $galpha, bool $marnm, bool $fams): Collection
69300ef1d3aSGreg Roach    {
69400ef1d3aSGreg Roach        $query = DB::table('individuals')
69500ef1d3aSGreg Roach            ->join('name', static function (JoinClause $join): void {
69600ef1d3aSGreg Roach                $join
69700ef1d3aSGreg Roach                    ->on('n_id', '=', 'i_id')
69800ef1d3aSGreg Roach                    ->on('n_file', '=', 'i_file');
69900ef1d3aSGreg Roach            })
70000ef1d3aSGreg Roach            ->where('i_file', '=', $tree->id())
70100ef1d3aSGreg Roach            ->select(['i_id AS xref', 'i_gedcom AS gedcom', 'n_givn', 'n_surn']);
70200ef1d3aSGreg Roach
70300ef1d3aSGreg Roach        $this->whereFamily($fams, $query);
70400ef1d3aSGreg Roach        $this->whereMarriedName($marnm, $query);
70500ef1d3aSGreg Roach
70600ef1d3aSGreg Roach        if ($surns_to_show === []) {
70700ef1d3aSGreg Roach            $query->whereNotIn('n_surn', ['', '@N.N.']);
70800ef1d3aSGreg Roach        } else {
70900ef1d3aSGreg Roach            $query->whereIn(DB::binaryColumn('n_surn'), $surns_to_show);
71000ef1d3aSGreg Roach        }
71100ef1d3aSGreg Roach
71200ef1d3aSGreg Roach        $individuals = new Collection();
71300ef1d3aSGreg Roach
71400ef1d3aSGreg Roach        foreach ($query->get() as $row) {
71500ef1d3aSGreg Roach            $individual = Registry::individualFactory()->make($row->xref, $tree, $row->gedcom);
71600ef1d3aSGreg Roach            assert($individual instanceof Individual);
71700ef1d3aSGreg Roach
71800ef1d3aSGreg Roach            // The name from the database may be private - check the filtered list...
719fd15e708SFranz Frese            foreach ($individual->getAllNames() as $n => $name) {
720*01b92fd6SGreg Roach                if ($name['givn'] === $row->n_givn && $name['surn'] === $row->n_surn) {
7212ed4bd58SFranz Frese                    if ($galpha === '' || I18N::language()->initialLetter(I18N::language()->normalize(I18N::strtoupper($row->n_givn))) === $galpha) {
72200ef1d3aSGreg Roach                        $individual->setPrimaryName($n);
72300ef1d3aSGreg Roach                        // We need to clone $individual, as we may have multiple references to the
72400ef1d3aSGreg Roach                        // same individual in this list, and the "primary name" would otherwise
72500ef1d3aSGreg Roach                        // be shared amongst all of them.
72600ef1d3aSGreg Roach                        $individuals->push(clone $individual);
72700ef1d3aSGreg Roach                        break;
72800ef1d3aSGreg Roach                    }
72900ef1d3aSGreg Roach                }
730fd15e708SFranz Frese            }
731*01b92fd6SGreg Roach        }
73200ef1d3aSGreg Roach
73300ef1d3aSGreg Roach        return $individuals;
73400ef1d3aSGreg Roach    }
73500ef1d3aSGreg Roach
73600ef1d3aSGreg Roach    /**
73700ef1d3aSGreg Roach     * Fetch a list of families with specified names
73800ef1d3aSGreg Roach     * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
73900ef1d3aSGreg Roach     * To search for names with no surnames, use $salpha=","
74000ef1d3aSGreg Roach     *
74100ef1d3aSGreg Roach     * @param array<string> $surnames if set, only fetch people with this n_surname
74200ef1d3aSGreg Roach     * @param string        $galpha   if set, only fetch given names starting with this letter
74300ef1d3aSGreg Roach     * @param bool          $marnm    if set, include married names
74400ef1d3aSGreg Roach     *
74500ef1d3aSGreg Roach     * @return Collection<int,Family>
74600ef1d3aSGreg Roach     */
74700ef1d3aSGreg Roach    protected function families(Tree $tree, array $surnames, string $galpha, bool $marnm): Collection
74800ef1d3aSGreg Roach    {
74900ef1d3aSGreg Roach        $families = new Collection();
75000ef1d3aSGreg Roach
75100ef1d3aSGreg Roach        foreach ($this->individuals($tree, $surnames, $galpha, $marnm, true) as $indi) {
75200ef1d3aSGreg Roach            foreach ($indi->spouseFamilies() as $family) {
75300ef1d3aSGreg Roach                $families->push($family);
75400ef1d3aSGreg Roach            }
75500ef1d3aSGreg Roach        }
75600ef1d3aSGreg Roach
75700ef1d3aSGreg Roach        return $families->unique();
75800ef1d3aSGreg Roach    }
75900ef1d3aSGreg Roach}
760