xref: /webtrees/app/Module/IndividualListModule.php (revision 06a438b41c4b328354bcb5bd8d8d578a3a78f995)
167992b6aSRichard Cissee<?php
23976b470SGreg Roach
367992b6aSRichard Cissee/**
467992b6aSRichard Cissee * webtrees: online genealogy
5*06a438b4SGreg Roach * Copyright (C) 2020 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
1567992b6aSRichard Cissee * along with this program. If not, see <http://www.gnu.org/licenses/>.
1667992b6aSRichard Cissee */
17fcfa147eSGreg Roach
1867992b6aSRichard Cisseedeclare(strict_types=1);
1967992b6aSRichard Cissee
2067992b6aSRichard Cisseenamespace Fisharebest\Webtrees\Module;
2167992b6aSRichard Cissee
22*06a438b4SGreg Roachuse Aura\Router\RouterContainer;
23*06a438b4SGreg Roachuse Fisharebest\Localization\Locale\LocaleInterface;
2467992b6aSRichard Cisseeuse Fisharebest\Webtrees\Auth;
25*06a438b4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
26*06a438b4SGreg Roachuse Fisharebest\Webtrees\Factory;
27*06a438b4SGreg Roachuse Fisharebest\Webtrees\Family;
28*06a438b4SGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsPrintLists;
29*06a438b4SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
30*06a438b4SGreg Roachuse Fisharebest\Webtrees\I18N;
31*06a438b4SGreg Roachuse Fisharebest\Webtrees\Individual;
32*06a438b4SGreg Roachuse Fisharebest\Webtrees\Services\LocalizationService;
33*06a438b4SGreg Roachuse Fisharebest\Webtrees\Session;
345229eadeSGreg Roachuse Fisharebest\Webtrees\Tree;
35*06a438b4SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
36*06a438b4SGreg Roachuse Illuminate\Database\Query\Builder;
37*06a438b4SGreg Roachuse Illuminate\Database\Query\Expression;
38*06a438b4SGreg Roachuse Illuminate\Database\Query\JoinClause;
396ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
406ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
41*06a438b4SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
42f3874e19SGreg Roach
43*06a438b4SGreg Roachuse function app;
44*06a438b4SGreg Roachuse function array_keys;
455229eadeSGreg Roachuse function assert;
46*06a438b4SGreg Roachuse function e;
47*06a438b4SGreg Roachuse function implode;
48*06a438b4SGreg Roachuse function ob_get_clean;
49*06a438b4SGreg Roachuse function ob_start;
50*06a438b4SGreg Roachuse function redirect;
51*06a438b4SGreg Roachuse function route;
52*06a438b4SGreg Roachuse function usort;
53*06a438b4SGreg Roachuse function view;
5467992b6aSRichard Cissee
5567992b6aSRichard Cissee/**
5667992b6aSRichard Cissee * Class IndividualListModule
5767992b6aSRichard Cissee */
58*06a438b4SGreg Roachclass IndividualListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
5967992b6aSRichard Cissee{
6067992b6aSRichard Cissee    use ModuleListTrait;
6167992b6aSRichard Cissee
62*06a438b4SGreg Roach    protected const ROUTE_URL  = '/tree/{tree}/individual-list';
63*06a438b4SGreg Roach
64*06a438b4SGreg Roach    /** @var LocalizationService */
65*06a438b4SGreg Roach    private $localization_service;
66*06a438b4SGreg Roach
67*06a438b4SGreg Roach    /**
68*06a438b4SGreg Roach     * IndividualListModule constructor.
69*06a438b4SGreg Roach     *
70*06a438b4SGreg Roach     * @param LocalizationService  $localization_service
71*06a438b4SGreg Roach     */
72*06a438b4SGreg Roach    public function __construct(LocalizationService $localization_service)
73*06a438b4SGreg Roach    {
74*06a438b4SGreg Roach        $this->localization_service = $localization_service;
75*06a438b4SGreg Roach    }
76*06a438b4SGreg Roach
77*06a438b4SGreg Roach    /**
78*06a438b4SGreg Roach     * Initialization.
79*06a438b4SGreg Roach     *
80*06a438b4SGreg Roach     * @return void
81*06a438b4SGreg Roach     */
82*06a438b4SGreg Roach    public function boot(): void
83*06a438b4SGreg Roach    {
84*06a438b4SGreg Roach        $router_container = app(RouterContainer::class);
85*06a438b4SGreg Roach        assert($router_container instanceof RouterContainer);
86*06a438b4SGreg Roach
87*06a438b4SGreg Roach        $router_container->getMap()
88*06a438b4SGreg Roach            ->get(static::class, static::ROUTE_URL, $this);
89*06a438b4SGreg Roach    }
90*06a438b4SGreg Roach
9167992b6aSRichard Cissee    /**
920cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
9367992b6aSRichard Cissee     *
9467992b6aSRichard Cissee     * @return string
9567992b6aSRichard Cissee     */
9667992b6aSRichard Cissee    public function title(): string
9767992b6aSRichard Cissee    {
9867992b6aSRichard Cissee        /* I18N: Name of a module/list */
9967992b6aSRichard Cissee        return I18N::translate('Individuals');
10067992b6aSRichard Cissee    }
10167992b6aSRichard Cissee
10267992b6aSRichard Cissee    /**
10367992b6aSRichard Cissee     * A sentence describing what this module does.
10467992b6aSRichard Cissee     *
10567992b6aSRichard Cissee     * @return string
10667992b6aSRichard Cissee     */
10767992b6aSRichard Cissee    public function description(): string
10867992b6aSRichard Cissee    {
109b5e8e56bSGreg Roach        /* I18N: Description of the “Individuals” module */
11067992b6aSRichard Cissee        return I18N::translate('A list of individuals.');
11167992b6aSRichard Cissee    }
11267992b6aSRichard Cissee
11367992b6aSRichard Cissee    /**
11467992b6aSRichard Cissee     * CSS class for the URL.
11567992b6aSRichard Cissee     *
11667992b6aSRichard Cissee     * @return string
11767992b6aSRichard Cissee     */
11867992b6aSRichard Cissee    public function listMenuClass(): string
11967992b6aSRichard Cissee    {
12067992b6aSRichard Cissee        return 'menu-list-indi';
12167992b6aSRichard Cissee    }
12267992b6aSRichard Cissee
1234db4b4a9SGreg Roach    /**
124*06a438b4SGreg Roach     * @param Tree    $tree
125*06a438b4SGreg Roach     * @param mixed[] $parameters
1264db4b4a9SGreg Roach     *
127*06a438b4SGreg Roach     * @return string
1284db4b4a9SGreg Roach     */
129*06a438b4SGreg Roach    public function listUrl(Tree $tree, array $parameters = []): string
13067992b6aSRichard Cissee    {
131*06a438b4SGreg Roach        $xref = app(ServerRequestInterface::class)->getAttribute('xref', '');
1325229eadeSGreg Roach
133*06a438b4SGreg Roach        if ($xref !== '') {
134*06a438b4SGreg Roach            $individual = Factory::individual()->make($xref, $tree);
13557ab2231SGreg Roach
136*06a438b4SGreg Roach            if ($individual instanceof Individual && $individual->canShow()) {
137*06a438b4SGreg Roach                $parameters['surname'] = $parameters['surname'] ?? $individual->getAllNames()[0]['surn'] ?? null;
138*06a438b4SGreg Roach            }
139*06a438b4SGreg Roach        }
14067992b6aSRichard Cissee
141*06a438b4SGreg Roach        $parameters['tree'] = $tree->name();
14257ab2231SGreg Roach
143*06a438b4SGreg Roach        return route(static::class, $parameters);
14467992b6aSRichard Cissee    }
14567992b6aSRichard Cissee
1464db4b4a9SGreg Roach    /**
1474db4b4a9SGreg Roach     * @return string[]
1484db4b4a9SGreg Roach     */
14967992b6aSRichard Cissee    public function listUrlAttributes(): array
15067992b6aSRichard Cissee    {
15167992b6aSRichard Cissee        return [];
15267992b6aSRichard Cissee    }
153*06a438b4SGreg Roach
154*06a438b4SGreg Roach    /**
155*06a438b4SGreg Roach     * Handle URLs generated by older versions of webtrees
156*06a438b4SGreg Roach     *
157*06a438b4SGreg Roach     * @param ServerRequestInterface $request
158*06a438b4SGreg Roach     *
159*06a438b4SGreg Roach     * @return ResponseInterface
160*06a438b4SGreg Roach     */
161*06a438b4SGreg Roach    public function getListAction(ServerRequestInterface $request): ResponseInterface
162*06a438b4SGreg Roach    {
163*06a438b4SGreg Roach        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
164*06a438b4SGreg Roach    }
165*06a438b4SGreg Roach
166*06a438b4SGreg Roach    /**
167*06a438b4SGreg Roach     * @param ServerRequestInterface $request
168*06a438b4SGreg Roach     *
169*06a438b4SGreg Roach     * @return ResponseInterface
170*06a438b4SGreg Roach     */
171*06a438b4SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
172*06a438b4SGreg Roach    {
173*06a438b4SGreg Roach        $tree = $request->getAttribute('tree');
174*06a438b4SGreg Roach        assert($tree instanceof Tree);
175*06a438b4SGreg Roach
176*06a438b4SGreg Roach        $user = $request->getAttribute('user');
177*06a438b4SGreg Roach        assert($user instanceof UserInterface);
178*06a438b4SGreg Roach
179*06a438b4SGreg Roach        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
180*06a438b4SGreg Roach
181*06a438b4SGreg Roach        return $this->createResponse($tree, $user, $request->getQueryParams(), false);
182*06a438b4SGreg Roach    }
183*06a438b4SGreg Roach
184*06a438b4SGreg Roach    /**
185*06a438b4SGreg Roach     * @param Tree                   $tree
186*06a438b4SGreg Roach     * @param UserInterface          $user
187*06a438b4SGreg Roach     * @param array<string>          $params
188*06a438b4SGreg Roach     * @param bool                   $families
189*06a438b4SGreg Roach     *
190*06a438b4SGreg Roach     * @return ResponseInterface
191*06a438b4SGreg Roach     */
192*06a438b4SGreg Roach    protected function createResponse(Tree $tree, UserInterface $user, array $params, bool $families): ResponseInterface
193*06a438b4SGreg Roach    {
194*06a438b4SGreg Roach        ob_start();
195*06a438b4SGreg Roach
196*06a438b4SGreg Roach        // We show three different lists: initials, surnames and individuals
197*06a438b4SGreg Roach
198*06a438b4SGreg Roach        // All surnames beginning with this letter where "@"=unknown and ","=none
199*06a438b4SGreg Roach        $alpha = $params['alpha'] ?? '';
200*06a438b4SGreg Roach
201*06a438b4SGreg Roach        // All individuals with this surname
202*06a438b4SGreg Roach        $surname = $params['surname'] ??  '';
203*06a438b4SGreg Roach
204*06a438b4SGreg Roach        // All individuals
205*06a438b4SGreg Roach        $show_all = $params['show_all'] ?? 'no';
206*06a438b4SGreg Roach
207*06a438b4SGreg Roach        // Long lists can be broken down by given name
208*06a438b4SGreg Roach        $show_all_firstnames = $params['show_all_firstnames'] ?? 'no';
209*06a438b4SGreg Roach        if ($show_all_firstnames === 'yes') {
210*06a438b4SGreg Roach            $falpha = '';
211*06a438b4SGreg Roach        } else {
212*06a438b4SGreg Roach            // All first names beginning with this letter
213*06a438b4SGreg Roach            $falpha = $params['falpha'] ?? '';
214*06a438b4SGreg Roach        }
215*06a438b4SGreg Roach
216*06a438b4SGreg Roach        $show_marnm = $params['show_marnm'] ?? '';
217*06a438b4SGreg Roach        switch ($show_marnm) {
218*06a438b4SGreg Roach            case 'no':
219*06a438b4SGreg Roach            case 'yes':
220*06a438b4SGreg Roach                $user->setPreference($families ? 'family-list-marnm' : 'individual-list-marnm', $show_marnm);
221*06a438b4SGreg Roach                break;
222*06a438b4SGreg Roach            default:
223*06a438b4SGreg Roach                $show_marnm = $user->getPreference($families ? 'family-list-marnm' : 'individual-list-marnm');
224*06a438b4SGreg Roach        }
225*06a438b4SGreg Roach
226*06a438b4SGreg Roach        // Make sure selections are consistent.
227*06a438b4SGreg Roach        // i.e. can’t specify show_all and surname at the same time.
228*06a438b4SGreg Roach        if ($show_all === 'yes') {
229*06a438b4SGreg Roach            if ($show_all_firstnames === 'yes') {
230*06a438b4SGreg Roach                $alpha   = '';
231*06a438b4SGreg Roach                $surname = '';
232*06a438b4SGreg Roach                $legend  = I18N::translate('All');
233*06a438b4SGreg Roach                $params  = [
234*06a438b4SGreg Roach                    'tree'     => $tree->name(),
235*06a438b4SGreg Roach                    'show_all' => 'yes',
236*06a438b4SGreg Roach                ];
237*06a438b4SGreg Roach                $show    = 'indi';
238*06a438b4SGreg Roach            } elseif ($falpha !== '') {
239*06a438b4SGreg Roach                $alpha   = '';
240*06a438b4SGreg Roach                $surname = '';
241*06a438b4SGreg Roach                $legend  = I18N::translate('All') . ', ' . e($falpha) . '…';
242*06a438b4SGreg Roach                $params  = [
243*06a438b4SGreg Roach                    'tree'      => $tree->name(),
244*06a438b4SGreg Roach                    'show_all' => 'yes',
245*06a438b4SGreg Roach                ];
246*06a438b4SGreg Roach                $show    = 'indi';
247*06a438b4SGreg Roach            } else {
248*06a438b4SGreg Roach                $alpha   = '';
249*06a438b4SGreg Roach                $surname = '';
250*06a438b4SGreg Roach                $legend  = I18N::translate('All');
251*06a438b4SGreg Roach                $params  = [
252*06a438b4SGreg Roach                    'tree'     => $tree->name(),
253*06a438b4SGreg Roach                    'show_all' => 'yes',
254*06a438b4SGreg Roach                ];
255*06a438b4SGreg Roach                $show    = $params['show'] ?? 'surn';
256*06a438b4SGreg Roach            }
257*06a438b4SGreg Roach        } elseif ($surname !== '') {
258*06a438b4SGreg Roach            $alpha    = $this->localization_service->initialLetter($surname, I18N::locale()); // so we can highlight the initial letter
259*06a438b4SGreg Roach            $show_all = 'no';
260*06a438b4SGreg Roach            if ($surname === '@N.N.') {
261*06a438b4SGreg Roach                $legend = I18N::translateContext('Unknown surname', '…');
262*06a438b4SGreg Roach            } else {
263*06a438b4SGreg Roach                // The surname parameter is a root/canonical form.
264*06a438b4SGreg Roach                // Display it as the actual surname
265*06a438b4SGreg Roach                $legend = implode('/', array_keys($this->surnames($tree, $surname, $alpha, $show_marnm === 'yes', $families, I18N::locale())));
266*06a438b4SGreg Roach            }
267*06a438b4SGreg Roach            $params = [
268*06a438b4SGreg Roach                'tree'    => $tree->name(),
269*06a438b4SGreg Roach                'surname' => $surname,
270*06a438b4SGreg Roach                'falpha'  => $falpha,
271*06a438b4SGreg Roach            ];
272*06a438b4SGreg Roach            switch ($falpha) {
273*06a438b4SGreg Roach                case '':
274*06a438b4SGreg Roach                    break;
275*06a438b4SGreg Roach                case '@':
276*06a438b4SGreg Roach                    $legend .= ', ' . I18N::translateContext('Unknown given name', '…');
277*06a438b4SGreg Roach                    break;
278*06a438b4SGreg Roach                default:
279*06a438b4SGreg Roach                    $legend .= ', ' . e($falpha) . '…';
280*06a438b4SGreg Roach                    break;
281*06a438b4SGreg Roach            }
282*06a438b4SGreg Roach            $show = 'indi'; // SURN list makes no sense here
283*06a438b4SGreg Roach        } elseif ($alpha === '@') {
284*06a438b4SGreg Roach            $show_all = 'no';
285*06a438b4SGreg Roach            $legend   = I18N::translateContext('Unknown surname', '…');
286*06a438b4SGreg Roach            $params   = [
287*06a438b4SGreg Roach                'alpha' => $alpha,
288*06a438b4SGreg Roach                'tree'   => $tree->name(),
289*06a438b4SGreg Roach            ];
290*06a438b4SGreg Roach            $show     = 'indi'; // SURN list makes no sense here
291*06a438b4SGreg Roach        } elseif ($alpha === ',') {
292*06a438b4SGreg Roach            $show_all = 'no';
293*06a438b4SGreg Roach            $legend   = I18N::translate('None');
294*06a438b4SGreg Roach            $params   = [
295*06a438b4SGreg Roach                'alpha' => $alpha,
296*06a438b4SGreg Roach                'tree'   => $tree->name(),
297*06a438b4SGreg Roach            ];
298*06a438b4SGreg Roach            $show     = 'indi'; // SURN list makes no sense here
299*06a438b4SGreg Roach        } elseif ($alpha !== '') {
300*06a438b4SGreg Roach            $show_all = 'no';
301*06a438b4SGreg Roach            $legend   = e($alpha) . '…';
302*06a438b4SGreg Roach            $params   = [
303*06a438b4SGreg Roach                'alpha' => $alpha,
304*06a438b4SGreg Roach                'tree'   => $tree->name(),
305*06a438b4SGreg Roach            ];
306*06a438b4SGreg Roach            $show     = $params['show'] ?? 'surn';
307*06a438b4SGreg Roach        } else {
308*06a438b4SGreg Roach            $show_all = 'no';
309*06a438b4SGreg Roach            $legend   = '…';
310*06a438b4SGreg Roach            $params   = [
311*06a438b4SGreg Roach                'tree' => $tree->name(),
312*06a438b4SGreg Roach            ];
313*06a438b4SGreg Roach            $show     = 'none'; // Don't show lists until something is chosen
314*06a438b4SGreg Roach        }
315*06a438b4SGreg Roach        $legend = '<span dir="auto">' . $legend . '</span>';
316*06a438b4SGreg Roach
317*06a438b4SGreg Roach        if ($families) {
318*06a438b4SGreg Roach            $title = I18N::translate('Families') . ' — ' . $legend;
319*06a438b4SGreg Roach        } else {
320*06a438b4SGreg Roach            $title = I18N::translate('Individuals') . ' — ' . $legend;
321*06a438b4SGreg Roach        } ?>
322*06a438b4SGreg Roach        <div class="d-flex flex-column wt-page-options wt-page-options-individual-list d-print-none">
323*06a438b4SGreg Roach            <ul class="d-flex flex-wrap list-unstyled justify-content-center wt-initials-list wt-initials-list-surname">
324*06a438b4SGreg Roach
325*06a438b4SGreg Roach                <?php foreach ($this->surnameAlpha($tree, $show_marnm === 'yes', $families, I18N::locale()) as $letter => $count) : ?>
326*06a438b4SGreg Roach                    <li class="wt-initials-list-item d-flex">
327*06a438b4SGreg Roach                        <?php if ($count > 0) : ?>
328*06a438b4SGreg Roach                            <a href="<?= e($this->listUrl($tree, ['alpha' => $letter, 'tree' => $tree->name()])) ?>" class="wt-initial px-1<?= $letter === $alpha ? ' active' : '' ?> '" title="<?= I18N::number($count) ?>"><?= $this->surnameInitial((string) $letter) ?></a>
329*06a438b4SGreg Roach                        <?php else : ?>
330*06a438b4SGreg Roach                            <span class="wt-initial px-1 text-muted"><?= $this->surnameInitial((string) $letter) ?></span>
331*06a438b4SGreg Roach
332*06a438b4SGreg Roach                        <?php endif ?>
333*06a438b4SGreg Roach                    </li>
334*06a438b4SGreg Roach                <?php endforeach ?>
335*06a438b4SGreg Roach
336*06a438b4SGreg Roach                <?php if (Session::has('initiated')) : ?>
337*06a438b4SGreg Roach                    <!-- Search spiders don't get the "show all" option as the other links give them everything. -->
338*06a438b4SGreg Roach                    <li class="wt-initials-list-item d-flex">
339*06a438b4SGreg Roach                        <a class="wt-initial px-1<?= $show_all === 'yes' ? ' active' : '' ?>" href="<?= e($this->listUrl($tree, ['show_all' => 'yes'] + $params)) ?>"><?= I18N::translate('All') ?></a>
340*06a438b4SGreg Roach                    </li>
341*06a438b4SGreg Roach                <?php endif ?>
342*06a438b4SGreg Roach            </ul>
343*06a438b4SGreg Roach
344*06a438b4SGreg 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 -->
345*06a438b4SGreg Roach            <?php if ($show !== 'none' && Session::has('initiated')) : ?>
346*06a438b4SGreg Roach                <?php if ($show_marnm === 'yes') : ?>
347*06a438b4SGreg Roach                    <p>
348*06a438b4SGreg Roach                        <a href="<?= e($this->listUrl($tree, ['show' => $show, 'show_marnm' => 'no'] + $params)) ?>">
349*06a438b4SGreg Roach                            <?= I18N::translate('Exclude individuals with “%s” as a married name', $legend) ?>
350*06a438b4SGreg Roach                        </a>
351*06a438b4SGreg Roach                    </p>
352*06a438b4SGreg Roach                <?php else : ?>
353*06a438b4SGreg Roach                    <p>
354*06a438b4SGreg Roach                        <a href="<?= e($this->listUrl($tree, ['show' => $show, 'show_marnm' => 'yes'] + $params)) ?>">
355*06a438b4SGreg Roach                            <?= I18N::translate('Include individuals with “%s” as a married name', $legend) ?>
356*06a438b4SGreg Roach                        </a>
357*06a438b4SGreg Roach                    </p>
358*06a438b4SGreg Roach                <?php endif ?>
359*06a438b4SGreg Roach
360*06a438b4SGreg Roach                <?php if ($alpha !== '@' && $alpha !== ',' && $surname === '') : ?>
361*06a438b4SGreg Roach                    <?php if ($show === 'surn') : ?>
362*06a438b4SGreg Roach                        <p>
363*06a438b4SGreg Roach                            <a href="<?= e($this->listUrl($tree, ['show' => 'indi', 'show_marnm' => 'no'] + $params)) ?>">
364*06a438b4SGreg Roach                                <?= I18N::translate('Show the list of individuals') ?>
365*06a438b4SGreg Roach                            </a>
366*06a438b4SGreg Roach                        </p>
367*06a438b4SGreg Roach                    <?php else : ?>
368*06a438b4SGreg Roach                        <p>
369*06a438b4SGreg Roach                            <a href="<?= e($this->listUrl($tree, ['show' => 'surn', 'show_marnm' => 'no'] + $params)) ?>">
370*06a438b4SGreg Roach                                <?= I18N::translate('Show the list of surnames') ?>
371*06a438b4SGreg Roach                            </a>
372*06a438b4SGreg Roach                        </p>
373*06a438b4SGreg Roach                    <?php endif ?>
374*06a438b4SGreg Roach                <?php endif ?>
375*06a438b4SGreg Roach            <?php endif ?>
376*06a438b4SGreg Roach        </div>
377*06a438b4SGreg Roach
378*06a438b4SGreg Roach        <div class="wt-page-content">
379*06a438b4SGreg Roach            <?php
380*06a438b4SGreg Roach
381*06a438b4SGreg Roach            if ($show === 'indi' || $show === 'surn') {
382*06a438b4SGreg Roach                $surns = $this->surnames($tree, $surname, $alpha, $show_marnm === 'yes', $families, I18N::locale());
383*06a438b4SGreg Roach                if ($show === 'surn') {
384*06a438b4SGreg Roach                    // Show the surname list
385*06a438b4SGreg Roach                    switch ($tree->getPreference('SURNAME_LIST_STYLE')) {
386*06a438b4SGreg Roach                        case 'style1':
387*06a438b4SGreg Roach                            echo FunctionsPrintLists::surnameList($surns, 3, true, $this, $tree);
388*06a438b4SGreg Roach                            break;
389*06a438b4SGreg Roach                        case 'style3':
390*06a438b4SGreg Roach                            echo FunctionsPrintLists::surnameTagCloud($surns, $this, true, $tree);
391*06a438b4SGreg Roach                            break;
392*06a438b4SGreg Roach                        case 'style2':
393*06a438b4SGreg Roach                        default:
394*06a438b4SGreg Roach                            echo view('lists/surnames-table', [
395*06a438b4SGreg Roach                                'surnames' => $surns,
396*06a438b4SGreg Roach                                'families' => $families,
397*06a438b4SGreg Roach                                'module'   => $this,
398*06a438b4SGreg Roach                                'tree'     => $tree,
399*06a438b4SGreg Roach                            ]);
400*06a438b4SGreg Roach                            break;
401*06a438b4SGreg Roach                    }
402*06a438b4SGreg Roach                } else {
403*06a438b4SGreg Roach                    // Show the list
404*06a438b4SGreg Roach                    $count = 0;
405*06a438b4SGreg Roach                    foreach ($surns as $surnames) {
406*06a438b4SGreg Roach                        foreach ($surnames as $total) {
407*06a438b4SGreg Roach                            $count += $total;
408*06a438b4SGreg Roach                        }
409*06a438b4SGreg Roach                    }
410*06a438b4SGreg Roach                    // Don't sublist short lists.
411*06a438b4SGreg Roach                    if ($count < $tree->getPreference('SUBLIST_TRIGGER_I')) {
412*06a438b4SGreg Roach                        $falpha = '';
413*06a438b4SGreg Roach                    } else {
414*06a438b4SGreg Roach                        $givn_initials = $this->givenAlpha($tree, $surname, $alpha, $show_marnm === 'yes', $families, I18N::locale());
415*06a438b4SGreg Roach                        // Break long lists by initial letter of given name
416*06a438b4SGreg Roach                        if ($surname !== '' || $show_all === 'yes') {
417*06a438b4SGreg Roach                            if ($show_all === 'no') {
418*06a438b4SGreg Roach                                echo '<h2 class="wt-page-title">', I18N::translate('Individuals with surname %s', $legend), '</h2>';
419*06a438b4SGreg Roach                            }
420*06a438b4SGreg Roach                            // Don't show the list until we have some filter criteria
421*06a438b4SGreg Roach                            $show = $falpha !== '' || $show_all_firstnames === 'yes' ? 'indi' : 'none';
422*06a438b4SGreg Roach                            $list = [];
423*06a438b4SGreg Roach                            echo '<ul class="d-flex flex-wrap list-unstyled justify-content-center wt-initials-list wt-initials-list-given-names">';
424*06a438b4SGreg Roach                            foreach ($givn_initials as $givn_initial => $given_count) {
425*06a438b4SGreg Roach                                echo '<li class="wt-initials-list-item d-flex">';
426*06a438b4SGreg Roach                                if ($given_count > 0) {
427*06a438b4SGreg Roach                                    if ($show === 'indi' && $givn_initial === $falpha && $show_all_firstnames === 'no') {
428*06a438b4SGreg Roach                                        echo '<a class="wt-initial px-1 active" href="' . e($this->listUrl($tree, ['falpha' => $givn_initial] + $params)) . '" title="' . I18N::number($given_count) . '">' . $this->givenNameInitial((string) $givn_initial) . '</a>';
429*06a438b4SGreg Roach                                    } else {
430*06a438b4SGreg Roach                                        echo '<a class="wt-initial px-1" href="' . e($this->listUrl($tree, ['falpha' => $givn_initial] + $params)) . '" title="' . I18N::number($given_count) . '">' . $this->givenNameInitial((string) $givn_initial) . '</a>';
431*06a438b4SGreg Roach                                    }
432*06a438b4SGreg Roach                                } else {
433*06a438b4SGreg Roach                                    echo '<span class="wt-initial px-1 text-muted">' . $this->givenNameInitial((string) $givn_initial) . '</span>';
434*06a438b4SGreg Roach                                }
435*06a438b4SGreg Roach                                echo '</li>';
436*06a438b4SGreg Roach                            }
437*06a438b4SGreg Roach                            // Search spiders don't get the "show all" option as the other links give them everything.
438*06a438b4SGreg Roach                            if (Session::has('initiated')) {
439*06a438b4SGreg Roach                                echo '<li class="wt-initials-list-item d-flex">';
440*06a438b4SGreg Roach                                if ($show_all_firstnames === 'yes') {
441*06a438b4SGreg Roach                                    echo '<span class="wt-initial px-1 warning">' . I18N::translate('All') . '</span>';
442*06a438b4SGreg Roach                                } else {
443*06a438b4SGreg 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>';
444*06a438b4SGreg Roach                                }
445*06a438b4SGreg Roach                                echo '</li>';
446*06a438b4SGreg Roach                            }
447*06a438b4SGreg Roach                            echo '</ul>';
448*06a438b4SGreg Roach                            echo '<p class="text-center alpha_index">', implode(' | ', $list), '</p>';
449*06a438b4SGreg Roach                        }
450*06a438b4SGreg Roach                    }
451*06a438b4SGreg Roach                    if ($show === 'indi') {
452*06a438b4SGreg Roach                        if (!$families) {
453*06a438b4SGreg Roach                            echo view('lists/individuals-table', [
454*06a438b4SGreg Roach                                'individuals' => $this->individuals($tree, $surname, $alpha, $falpha, $show_marnm === 'yes', false, I18N::locale()),
455*06a438b4SGreg Roach                                'sosa'        => false,
456*06a438b4SGreg Roach                                'tree'        => $tree,
457*06a438b4SGreg Roach                            ]);
458*06a438b4SGreg Roach                        } else {
459*06a438b4SGreg Roach                            echo view('lists/families-table', [
460*06a438b4SGreg Roach                                'families' => $this->families($tree, $surname, $alpha, $falpha, $show_marnm === 'yes', I18N::locale()),
461*06a438b4SGreg Roach                                'tree'     => $tree,
462*06a438b4SGreg Roach                            ]);
463*06a438b4SGreg Roach                        }
464*06a438b4SGreg Roach                    }
465*06a438b4SGreg Roach                }
466*06a438b4SGreg Roach            } ?>
467*06a438b4SGreg Roach        </div>
468*06a438b4SGreg Roach        <?php
469*06a438b4SGreg Roach
470*06a438b4SGreg Roach        $html = ob_get_clean();
471*06a438b4SGreg Roach
472*06a438b4SGreg Roach        return $this->viewResponse('modules/individual-list/page', [
473*06a438b4SGreg Roach            'content' => $html,
474*06a438b4SGreg Roach            'title'   => $title,
475*06a438b4SGreg Roach            'tree'    => $tree,
476*06a438b4SGreg Roach        ]);
477*06a438b4SGreg Roach    }
478*06a438b4SGreg Roach
479*06a438b4SGreg Roach    /**
480*06a438b4SGreg Roach     * Some initial letters have a special meaning
481*06a438b4SGreg Roach     *
482*06a438b4SGreg Roach     * @param string $initial
483*06a438b4SGreg Roach     *
484*06a438b4SGreg Roach     * @return string
485*06a438b4SGreg Roach     */
486*06a438b4SGreg Roach    protected function givenNameInitial(string $initial): string
487*06a438b4SGreg Roach    {
488*06a438b4SGreg Roach        if ($initial === '@') {
489*06a438b4SGreg Roach            return I18N::translateContext('Unknown given name', '…');
490*06a438b4SGreg Roach        }
491*06a438b4SGreg Roach
492*06a438b4SGreg Roach        return e($initial);
493*06a438b4SGreg Roach    }
494*06a438b4SGreg Roach
495*06a438b4SGreg Roach    /**
496*06a438b4SGreg Roach     * Some initial letters have a special meaning
497*06a438b4SGreg Roach     *
498*06a438b4SGreg Roach     * @param string $initial
499*06a438b4SGreg Roach     *
500*06a438b4SGreg Roach     * @return string
501*06a438b4SGreg Roach     */
502*06a438b4SGreg Roach    protected function surnameInitial(string $initial): string
503*06a438b4SGreg Roach    {
504*06a438b4SGreg Roach        if ($initial === '@') {
505*06a438b4SGreg Roach            return I18N::translateContext('Unknown surname', '…');
506*06a438b4SGreg Roach        }
507*06a438b4SGreg Roach
508*06a438b4SGreg Roach        if ($initial === ',') {
509*06a438b4SGreg Roach            return I18N::translate('None');
510*06a438b4SGreg Roach        }
511*06a438b4SGreg Roach
512*06a438b4SGreg Roach        return e($initial);
513*06a438b4SGreg Roach    }
514*06a438b4SGreg Roach
515*06a438b4SGreg Roach    /**
516*06a438b4SGreg Roach     * Restrict a query to individuals that are a spouse in a family record.
517*06a438b4SGreg Roach     *
518*06a438b4SGreg Roach     * @param bool    $fams
519*06a438b4SGreg Roach     * @param Builder $query
520*06a438b4SGreg Roach     */
521*06a438b4SGreg Roach    protected function whereFamily(bool $fams, Builder $query): void
522*06a438b4SGreg Roach    {
523*06a438b4SGreg Roach        if ($fams) {
524*06a438b4SGreg Roach            $query->join('link', static function (JoinClause $join): void {
525*06a438b4SGreg Roach                $join
526*06a438b4SGreg Roach                    ->on('l_from', '=', 'n_id')
527*06a438b4SGreg Roach                    ->on('l_file', '=', 'n_file')
528*06a438b4SGreg Roach                    ->where('l_type', '=', 'FAMS');
529*06a438b4SGreg Roach            });
530*06a438b4SGreg Roach        }
531*06a438b4SGreg Roach    }
532*06a438b4SGreg Roach
533*06a438b4SGreg Roach    /**
534*06a438b4SGreg Roach     * Restrict a query to include/exclude married names.
535*06a438b4SGreg Roach     *
536*06a438b4SGreg Roach     * @param bool    $marnm
537*06a438b4SGreg Roach     * @param Builder $query
538*06a438b4SGreg Roach     */
539*06a438b4SGreg Roach    protected function whereMarriedName(bool $marnm, Builder $query): void
540*06a438b4SGreg Roach    {
541*06a438b4SGreg Roach        if (!$marnm) {
542*06a438b4SGreg Roach            $query->where('n_type', '<>', '_MARNM');
543*06a438b4SGreg Roach        }
544*06a438b4SGreg Roach    }
545*06a438b4SGreg Roach
546*06a438b4SGreg Roach    /**
547*06a438b4SGreg Roach     * Get a list of initial surname letters.
548*06a438b4SGreg Roach     *
549*06a438b4SGreg Roach     * @param Tree            $tree
550*06a438b4SGreg Roach     * @param bool            $marnm if set, include married names
551*06a438b4SGreg Roach     * @param bool            $fams  if set, only consider individuals with FAMS records
552*06a438b4SGreg Roach     * @param LocaleInterface $locale
553*06a438b4SGreg Roach     *
554*06a438b4SGreg Roach     * @return int[]
555*06a438b4SGreg Roach     */
556*06a438b4SGreg Roach    public function surnameAlpha(Tree $tree, bool $marnm, bool $fams, LocaleInterface $locale): array
557*06a438b4SGreg Roach    {
558*06a438b4SGreg Roach        $collation = $this->localization_service->collation($locale);
559*06a438b4SGreg Roach
560*06a438b4SGreg Roach        $n_surn = $this->fieldWithCollation('n_surn', $collation);
561*06a438b4SGreg Roach        $alphas = [];
562*06a438b4SGreg Roach
563*06a438b4SGreg Roach        $query = DB::table('name')->where('n_file', '=', $tree->id());
564*06a438b4SGreg Roach
565*06a438b4SGreg Roach        $this->whereFamily($fams, $query);
566*06a438b4SGreg Roach        $this->whereMarriedName($marnm, $query);
567*06a438b4SGreg Roach
568*06a438b4SGreg Roach        // Fetch all the letters in our alphabet, whether or not there
569*06a438b4SGreg Roach        // are any names beginning with that letter. It looks better to
570*06a438b4SGreg Roach        // show the full alphabet, rather than omitting rare letters such as X.
571*06a438b4SGreg Roach        foreach ($this->localization_service->alphabet($locale) as $letter) {
572*06a438b4SGreg Roach            $query2 = clone $query;
573*06a438b4SGreg Roach
574*06a438b4SGreg Roach            $this->whereInitial($query2, 'n_surn', $letter, $locale);
575*06a438b4SGreg Roach
576*06a438b4SGreg Roach            $alphas[$letter] = $query2->count();
577*06a438b4SGreg Roach        }
578*06a438b4SGreg Roach
579*06a438b4SGreg Roach        // Now fetch initial letters that are not in our alphabet,
580*06a438b4SGreg Roach        // including "@" (for "@N.N.") and "" for no surname.
581*06a438b4SGreg Roach        $query2 = clone $query;
582*06a438b4SGreg Roach        foreach ($this->localization_service->alphabet($locale) as $n => $letter) {
583*06a438b4SGreg Roach            $query2->where($n_surn, 'NOT LIKE', $letter . '%');
584*06a438b4SGreg Roach        }
585*06a438b4SGreg Roach
586*06a438b4SGreg Roach        $rows = $query2
587*06a438b4SGreg Roach            ->groupBy(['initial'])
588*06a438b4SGreg Roach            ->orderBy(new Expression("CASE initial WHEN '' THEN 1 ELSE 0 END"))
589*06a438b4SGreg Roach            ->orderBy(new Expression("CASE initial WHEN '@' THEN 1 ELSE 0 END"))
590*06a438b4SGreg Roach            ->orderBy('initial')
591*06a438b4SGreg Roach            ->pluck(new Expression('COUNT(*) AS aggregate'), new Expression('SUBSTR(n_surn, 1, 1) AS initial'));
592*06a438b4SGreg Roach
593*06a438b4SGreg Roach        foreach ($rows as $alpha => $count) {
594*06a438b4SGreg Roach            $alphas[$alpha] = (int) $count;
595*06a438b4SGreg Roach        }
596*06a438b4SGreg Roach
597*06a438b4SGreg Roach        $count_no_surname = $query->where('n_surn', '=', '')->count();
598*06a438b4SGreg Roach
599*06a438b4SGreg Roach        if ($count_no_surname !== 0) {
600*06a438b4SGreg Roach            // Special code to indicate "no surname"
601*06a438b4SGreg Roach            $alphas[','] = $count_no_surname;
602*06a438b4SGreg Roach        }
603*06a438b4SGreg Roach
604*06a438b4SGreg Roach        return $alphas;
605*06a438b4SGreg Roach    }
606*06a438b4SGreg Roach
607*06a438b4SGreg Roach    /**
608*06a438b4SGreg Roach     * Get a list of initial given name letters for indilist.php and famlist.php
609*06a438b4SGreg Roach     *
610*06a438b4SGreg Roach     * @param Tree            $tree
611*06a438b4SGreg Roach     * @param string          $surn   if set, only consider people with this surname
612*06a438b4SGreg Roach     * @param string          $salpha if set, only consider surnames starting with this letter
613*06a438b4SGreg Roach     * @param bool            $marnm  if set, include married names
614*06a438b4SGreg Roach     * @param bool            $fams   if set, only consider individuals with FAMS records
615*06a438b4SGreg Roach     * @param LocaleInterface $locale
616*06a438b4SGreg Roach     *
617*06a438b4SGreg Roach     * @return int[]
618*06a438b4SGreg Roach     */
619*06a438b4SGreg Roach    public function givenAlpha(Tree $tree, string $surn, string $salpha, bool $marnm, bool $fams, LocaleInterface $locale): array
620*06a438b4SGreg Roach    {
621*06a438b4SGreg Roach        $collation = $this->localization_service->collation($locale);
622*06a438b4SGreg Roach
623*06a438b4SGreg Roach        $alphas = [];
624*06a438b4SGreg Roach
625*06a438b4SGreg Roach        $query = DB::table('name')
626*06a438b4SGreg Roach            ->where('n_file', '=', $tree->id());
627*06a438b4SGreg Roach
628*06a438b4SGreg Roach        $this->whereFamily($fams, $query);
629*06a438b4SGreg Roach        $this->whereMarriedName($marnm, $query);
630*06a438b4SGreg Roach
631*06a438b4SGreg Roach        if ($surn !== '') {
632*06a438b4SGreg Roach            $n_surn = $this->fieldWithCollation('n_surn', $collation);
633*06a438b4SGreg Roach            $query->where($n_surn, '=', $surn);
634*06a438b4SGreg Roach        } elseif ($salpha === ',') {
635*06a438b4SGreg Roach            $query->where('n_surn', '=', '');
636*06a438b4SGreg Roach        } elseif ($salpha === '@') {
637*06a438b4SGreg Roach            $query->where('n_surn', '=', '@N.N.');
638*06a438b4SGreg Roach        } elseif ($salpha !== '') {
639*06a438b4SGreg Roach            $this->whereInitial($query, 'n_surn', $salpha, $locale);
640*06a438b4SGreg Roach        } else {
641*06a438b4SGreg Roach            // All surnames
642*06a438b4SGreg Roach            $query->whereNotIn('n_surn', ['', '@N.N.']);
643*06a438b4SGreg Roach        }
644*06a438b4SGreg Roach
645*06a438b4SGreg Roach        // Fetch all the letters in our alphabet, whether or not there
646*06a438b4SGreg Roach        // are any names beginning with that letter. It looks better to
647*06a438b4SGreg Roach        // show the full alphabet, rather than omitting rare letters such as X
648*06a438b4SGreg Roach        foreach ($this->localization_service->alphabet($locale) as $letter) {
649*06a438b4SGreg Roach            $query2 = clone $query;
650*06a438b4SGreg Roach
651*06a438b4SGreg Roach            $this->whereInitial($query2, 'n_givn', $letter, $locale);
652*06a438b4SGreg Roach
653*06a438b4SGreg Roach            $alphas[$letter] = $query2->distinct()->count('n_id');
654*06a438b4SGreg Roach        }
655*06a438b4SGreg Roach
656*06a438b4SGreg Roach        $rows = $query
657*06a438b4SGreg Roach            ->groupBy(['initial'])
658*06a438b4SGreg Roach            ->orderBy(new Expression("CASE initial WHEN '' THEN 1 ELSE 0 END"))
659*06a438b4SGreg Roach            ->orderBy(new Expression("CASE initial WHEN '@' THEN 1 ELSE 0 END"))
660*06a438b4SGreg Roach            ->orderBy('initial')
661*06a438b4SGreg Roach            ->pluck(new Expression('COUNT(*) AS aggregate'), new Expression('UPPER(SUBSTR(n_givn, 1, 1)) AS initial'));
662*06a438b4SGreg Roach
663*06a438b4SGreg Roach        foreach ($rows as $alpha => $count) {
664*06a438b4SGreg Roach            $alphas[$alpha] = (int) $count;
665*06a438b4SGreg Roach        }
666*06a438b4SGreg Roach
667*06a438b4SGreg Roach        return $alphas;
668*06a438b4SGreg Roach    }
669*06a438b4SGreg Roach
670*06a438b4SGreg Roach    /**
671*06a438b4SGreg Roach     * Get a count of actual surnames and variants, based on a "root" surname.
672*06a438b4SGreg Roach     *
673*06a438b4SGreg Roach     * @param Tree            $tree
674*06a438b4SGreg Roach     * @param string          $surn   if set, only count people with this surname
675*06a438b4SGreg Roach     * @param string          $salpha if set, only consider surnames starting with this letter
676*06a438b4SGreg Roach     * @param bool            $marnm  if set, include married names
677*06a438b4SGreg Roach     * @param bool            $fams   if set, only consider individuals with FAMS records
678*06a438b4SGreg Roach     * @param LocaleInterface $locale
679*06a438b4SGreg Roach     *
680*06a438b4SGreg Roach     * @return int[][]
681*06a438b4SGreg Roach     */
682*06a438b4SGreg Roach    public function surnames(
683*06a438b4SGreg Roach        Tree $tree,
684*06a438b4SGreg Roach        string $surn,
685*06a438b4SGreg Roach        string $salpha,
686*06a438b4SGreg Roach        bool $marnm,
687*06a438b4SGreg Roach        bool $fams,
688*06a438b4SGreg Roach        LocaleInterface $locale
689*06a438b4SGreg Roach    ): array {
690*06a438b4SGreg Roach        $collation = $this->localization_service->collation($locale);
691*06a438b4SGreg Roach
692*06a438b4SGreg Roach        $query = DB::table('name')
693*06a438b4SGreg Roach            ->where('n_file', '=', $tree->id())
694*06a438b4SGreg Roach            ->select([
695*06a438b4SGreg Roach                new Expression('UPPER(n_surn /*! COLLATE ' . $collation . ' */) AS n_surn'),
696*06a438b4SGreg Roach                new Expression('n_surname /*! COLLATE utf8_bin */ AS n_surname'),
697*06a438b4SGreg Roach                new Expression('COUNT(*) AS total'),
698*06a438b4SGreg Roach            ]);
699*06a438b4SGreg Roach
700*06a438b4SGreg Roach        $this->whereFamily($fams, $query);
701*06a438b4SGreg Roach        $this->whereMarriedName($marnm, $query);
702*06a438b4SGreg Roach
703*06a438b4SGreg Roach        if ($surn !== '') {
704*06a438b4SGreg Roach            $query->where('n_surn', '=', $surn);
705*06a438b4SGreg Roach        } elseif ($salpha === ',') {
706*06a438b4SGreg Roach            $query->where('n_surn', '=', '');
707*06a438b4SGreg Roach        } elseif ($salpha === '@') {
708*06a438b4SGreg Roach            $query->where('n_surn', '=', '@N.N.');
709*06a438b4SGreg Roach        } elseif ($salpha !== '') {
710*06a438b4SGreg Roach            $this->whereInitial($query, 'n_surn', $salpha, $locale);
711*06a438b4SGreg Roach        } else {
712*06a438b4SGreg Roach            // All surnames
713*06a438b4SGreg Roach            $query->whereNotIn('n_surn', ['', '@N.N.']);
714*06a438b4SGreg Roach        }
715*06a438b4SGreg Roach        $query
716*06a438b4SGreg Roach            ->groupBy(['n_surn'])
717*06a438b4SGreg Roach            ->groupBy(['n_surname'])
718*06a438b4SGreg Roach            ->orderBy('n_surname');
719*06a438b4SGreg Roach
720*06a438b4SGreg Roach        $list = [];
721*06a438b4SGreg Roach
722*06a438b4SGreg Roach        foreach ($query->get() as $row) {
723*06a438b4SGreg Roach            $list[$row->n_surn][$row->n_surname] = (int) $row->total;
724*06a438b4SGreg Roach        }
725*06a438b4SGreg Roach
726*06a438b4SGreg Roach        return $list;
727*06a438b4SGreg Roach    }
728*06a438b4SGreg Roach
729*06a438b4SGreg Roach    /**
730*06a438b4SGreg Roach     * Fetch a list of individuals with specified names
731*06a438b4SGreg Roach     * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
732*06a438b4SGreg Roach     * To search for names with no surnames, use $salpha=","
733*06a438b4SGreg Roach     *
734*06a438b4SGreg Roach     * @param Tree            $tree
735*06a438b4SGreg Roach     * @param string          $surn   if set, only fetch people with this surname
736*06a438b4SGreg Roach     * @param string          $salpha if set, only fetch surnames starting with this letter
737*06a438b4SGreg Roach     * @param string          $galpha if set, only fetch given names starting with this letter
738*06a438b4SGreg Roach     * @param bool            $marnm  if set, include married names
739*06a438b4SGreg Roach     * @param bool            $fams   if set, only fetch individuals with FAMS records
740*06a438b4SGreg Roach     * @param LocaleInterface $locale
741*06a438b4SGreg Roach     *
742*06a438b4SGreg Roach     * @return Individual[]
743*06a438b4SGreg Roach     */
744*06a438b4SGreg Roach    public function individuals(
745*06a438b4SGreg Roach        Tree $tree,
746*06a438b4SGreg Roach        string $surn,
747*06a438b4SGreg Roach        string $salpha,
748*06a438b4SGreg Roach        string $galpha,
749*06a438b4SGreg Roach        bool $marnm,
750*06a438b4SGreg Roach        bool $fams,
751*06a438b4SGreg Roach        LocaleInterface $locale
752*06a438b4SGreg Roach    ): array {
753*06a438b4SGreg Roach        $collation = $this->localization_service->collation($locale);
754*06a438b4SGreg Roach
755*06a438b4SGreg Roach        // Use specific collation for name fields.
756*06a438b4SGreg Roach        $n_givn = $this->fieldWithCollation('n_givn', $collation);
757*06a438b4SGreg Roach        $n_surn = $this->fieldWithCollation('n_surn', $collation);
758*06a438b4SGreg Roach
759*06a438b4SGreg Roach        $query = DB::table('individuals')
760*06a438b4SGreg Roach            ->join('name', static function (JoinClause $join): void {
761*06a438b4SGreg Roach                $join
762*06a438b4SGreg Roach                    ->on('n_id', '=', 'i_id')
763*06a438b4SGreg Roach                    ->on('n_file', '=', 'i_file');
764*06a438b4SGreg Roach            })
765*06a438b4SGreg Roach            ->where('i_file', '=', $tree->id())
766*06a438b4SGreg Roach            ->select(['i_id AS xref', 'i_gedcom AS gedcom', 'n_givn', 'n_surn']);
767*06a438b4SGreg Roach
768*06a438b4SGreg Roach        $this->whereFamily($fams, $query);
769*06a438b4SGreg Roach        $this->whereMarriedName($marnm, $query);
770*06a438b4SGreg Roach
771*06a438b4SGreg Roach        if ($surn) {
772*06a438b4SGreg Roach            $query->where($n_surn, '=', $surn);
773*06a438b4SGreg Roach        } elseif ($salpha === ',') {
774*06a438b4SGreg Roach            $query->where($n_surn, '=', '');
775*06a438b4SGreg Roach        } elseif ($salpha === '@') {
776*06a438b4SGreg Roach            $query->where($n_surn, '=', '@N.N.');
777*06a438b4SGreg Roach        } elseif ($salpha) {
778*06a438b4SGreg Roach            $this->whereInitial($query, 'n_surn', $salpha, $locale);
779*06a438b4SGreg Roach        } else {
780*06a438b4SGreg Roach            // All surnames
781*06a438b4SGreg Roach            $query->whereNotIn($n_surn, ['', '@N.N.']);
782*06a438b4SGreg Roach        }
783*06a438b4SGreg Roach        if ($galpha) {
784*06a438b4SGreg Roach            $this->whereInitial($query, 'n_givn', $galpha, $locale);
785*06a438b4SGreg Roach        }
786*06a438b4SGreg Roach
787*06a438b4SGreg Roach        $query
788*06a438b4SGreg Roach            ->orderBy(new Expression("CASE n_surn WHEN '@N.N.' THEN 1 ELSE 0 END"))
789*06a438b4SGreg Roach            ->orderBy($n_surn)
790*06a438b4SGreg Roach            ->orderBy(new Expression("CASE n_givn WHEN '@N.N.' THEN 1 ELSE 0 END"))
791*06a438b4SGreg Roach            ->orderBy($n_givn);
792*06a438b4SGreg Roach
793*06a438b4SGreg Roach        $list = [];
794*06a438b4SGreg Roach        $rows = $query->get();
795*06a438b4SGreg Roach
796*06a438b4SGreg Roach        foreach ($rows as $row) {
797*06a438b4SGreg Roach            $individual = Factory::individual()->make($row->xref, $tree, $row->gedcom);
798*06a438b4SGreg Roach            assert($individual instanceof Individual);
799*06a438b4SGreg Roach
800*06a438b4SGreg Roach            // The name from the database may be private - check the filtered list...
801*06a438b4SGreg Roach            foreach ($individual->getAllNames() as $n => $name) {
802*06a438b4SGreg Roach                if ($name['givn'] === $row->n_givn && $name['surn'] === $row->n_surn) {
803*06a438b4SGreg Roach                    $individual->setPrimaryName($n);
804*06a438b4SGreg Roach                    // We need to clone $individual, as we may have multiple references to the
805*06a438b4SGreg Roach                    // same individual in this list, and the "primary name" would otherwise
806*06a438b4SGreg Roach                    // be shared amongst all of them.
807*06a438b4SGreg Roach                    $list[] = clone $individual;
808*06a438b4SGreg Roach                    break;
809*06a438b4SGreg Roach                }
810*06a438b4SGreg Roach            }
811*06a438b4SGreg Roach        }
812*06a438b4SGreg Roach
813*06a438b4SGreg Roach        return $list;
814*06a438b4SGreg Roach    }
815*06a438b4SGreg Roach
816*06a438b4SGreg Roach    /**
817*06a438b4SGreg Roach     * Fetch a list of families with specified names
818*06a438b4SGreg Roach     * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
819*06a438b4SGreg Roach     * To search for names with no surnames, use $salpha=","
820*06a438b4SGreg Roach     *
821*06a438b4SGreg Roach     * @param Tree            $tree
822*06a438b4SGreg Roach     * @param string          $surn   if set, only fetch people with this surname
823*06a438b4SGreg Roach     * @param string          $salpha if set, only fetch surnames starting with this letter
824*06a438b4SGreg Roach     * @param string          $galpha if set, only fetch given names starting with this letter
825*06a438b4SGreg Roach     * @param bool            $marnm  if set, include married names
826*06a438b4SGreg Roach     * @param LocaleInterface $locale
827*06a438b4SGreg Roach     *
828*06a438b4SGreg Roach     * @return Family[]
829*06a438b4SGreg Roach     */
830*06a438b4SGreg Roach    public function families(Tree $tree, $surn, $salpha, $galpha, $marnm, LocaleInterface $locale): array
831*06a438b4SGreg Roach    {
832*06a438b4SGreg Roach        $list = [];
833*06a438b4SGreg Roach        foreach ($this->individuals($tree, $surn, $salpha, $galpha, $marnm, true, $locale) as $indi) {
834*06a438b4SGreg Roach            foreach ($indi->spouseFamilies() as $family) {
835*06a438b4SGreg Roach                $list[$family->xref()] = $family;
836*06a438b4SGreg Roach            }
837*06a438b4SGreg Roach        }
838*06a438b4SGreg Roach        usort($list, GedcomRecord::nameComparator());
839*06a438b4SGreg Roach
840*06a438b4SGreg Roach        return $list;
841*06a438b4SGreg Roach    }
842*06a438b4SGreg Roach
843*06a438b4SGreg Roach    /**
844*06a438b4SGreg Roach     * Use MySQL-specific comments so we can run these queries on other RDBMS.
845*06a438b4SGreg Roach     *
846*06a438b4SGreg Roach     * @param string $field
847*06a438b4SGreg Roach     * @param string $collation
848*06a438b4SGreg Roach     *
849*06a438b4SGreg Roach     * @return Expression
850*06a438b4SGreg Roach     */
851*06a438b4SGreg Roach    protected function fieldWithCollation(string $field, string $collation): Expression
852*06a438b4SGreg Roach    {
853*06a438b4SGreg Roach        return new Expression($field . ' /*! COLLATE ' . $collation . ' */');
854*06a438b4SGreg Roach    }
855*06a438b4SGreg Roach
856*06a438b4SGreg Roach    /**
857*06a438b4SGreg Roach     * Modify a query to restrict a field to a given initial letter.
858*06a438b4SGreg Roach     * Take account of digraphs, equialent letters, etc.
859*06a438b4SGreg Roach     *
860*06a438b4SGreg Roach     * @param Builder         $query
861*06a438b4SGreg Roach     * @param string          $field
862*06a438b4SGreg Roach     * @param string          $letter
863*06a438b4SGreg Roach     * @param LocaleInterface $locale
864*06a438b4SGreg Roach     *
865*06a438b4SGreg Roach     * @return void
866*06a438b4SGreg Roach     */
867*06a438b4SGreg Roach    protected function whereInitial(
868*06a438b4SGreg Roach        Builder $query,
869*06a438b4SGreg Roach        string $field,
870*06a438b4SGreg Roach        string $letter,
871*06a438b4SGreg Roach        LocaleInterface $locale
872*06a438b4SGreg Roach    ): void {
873*06a438b4SGreg Roach        $collation = $this->localization_service->collation($locale);
874*06a438b4SGreg Roach
875*06a438b4SGreg Roach        // Use MySQL-specific comments so we can run these queries on other RDBMS.
876*06a438b4SGreg Roach        $field_with_collation = $this->fieldWithCollation($field, $collation);
877*06a438b4SGreg Roach
878*06a438b4SGreg Roach        switch ($locale->languageTag()) {
879*06a438b4SGreg Roach            case 'cs':
880*06a438b4SGreg Roach                $this->whereInitialCzech($query, $field_with_collation, $letter);
881*06a438b4SGreg Roach                break;
882*06a438b4SGreg Roach
883*06a438b4SGreg Roach            case 'da':
884*06a438b4SGreg Roach            case 'nb':
885*06a438b4SGreg Roach            case 'nn':
886*06a438b4SGreg Roach                $this->whereInitialNorwegian($query, $field_with_collation, $letter);
887*06a438b4SGreg Roach                break;
888*06a438b4SGreg Roach
889*06a438b4SGreg Roach            case 'sv':
890*06a438b4SGreg Roach            case 'fi':
891*06a438b4SGreg Roach                $this->whereInitialSwedish($query, $field_with_collation, $letter);
892*06a438b4SGreg Roach                break;
893*06a438b4SGreg Roach
894*06a438b4SGreg Roach            case 'hu':
895*06a438b4SGreg Roach                $this->whereInitialHungarian($query, $field_with_collation, $letter);
896*06a438b4SGreg Roach                break;
897*06a438b4SGreg Roach
898*06a438b4SGreg Roach            case 'nl':
899*06a438b4SGreg Roach                $this->whereInitialDutch($query, $field_with_collation, $letter);
900*06a438b4SGreg Roach                break;
901*06a438b4SGreg Roach
902*06a438b4SGreg Roach            default:
903*06a438b4SGreg Roach                $query->where($field_with_collation, 'LIKE', '\\' . $letter . '%');
904*06a438b4SGreg Roach        }
905*06a438b4SGreg Roach    }
906*06a438b4SGreg Roach
907*06a438b4SGreg Roach    /**
908*06a438b4SGreg Roach     * @param Builder    $query
909*06a438b4SGreg Roach     * @param Expression $field
910*06a438b4SGreg Roach     * @param string     $letter
911*06a438b4SGreg Roach     */
912*06a438b4SGreg Roach    protected function whereInitialCzech(Builder $query, Expression $field, string $letter): void
913*06a438b4SGreg Roach    {
914*06a438b4SGreg Roach        if ($letter === 'C') {
915*06a438b4SGreg Roach            $query->where($field, 'LIKE', 'C%')->where($field, 'NOT LIKE', 'CH%');
916*06a438b4SGreg Roach        } else {
917*06a438b4SGreg Roach            $query->where($field, 'LIKE', '\\' . $letter . '%');
918*06a438b4SGreg Roach        }
919*06a438b4SGreg Roach    }
920*06a438b4SGreg Roach
921*06a438b4SGreg Roach    /**
922*06a438b4SGreg Roach     * @param Builder    $query
923*06a438b4SGreg Roach     * @param Expression $field
924*06a438b4SGreg Roach     * @param string     $letter
925*06a438b4SGreg Roach     */
926*06a438b4SGreg Roach    protected function whereInitialDutch(Builder $query, Expression $field, string $letter): void
927*06a438b4SGreg Roach    {
928*06a438b4SGreg Roach        if ($letter === 'I') {
929*06a438b4SGreg Roach            $query->where($field, 'LIKE', 'I%')->where($field, 'NOT LIKE', 'IJ%');
930*06a438b4SGreg Roach        } else {
931*06a438b4SGreg Roach            $query->where($field, 'LIKE', '\\' . $letter . '%');
932*06a438b4SGreg Roach        }
933*06a438b4SGreg Roach    }
934*06a438b4SGreg Roach
935*06a438b4SGreg Roach    /**
936*06a438b4SGreg Roach     * Hungarian has many digraphs and trigraphs, so exclude these from prefixes.
937*06a438b4SGreg Roach     *
938*06a438b4SGreg Roach     * @param Builder    $query
939*06a438b4SGreg Roach     * @param Expression $field
940*06a438b4SGreg Roach     * @param string     $letter
941*06a438b4SGreg Roach     */
942*06a438b4SGreg Roach    protected function whereInitialHungarian(Builder $query, Expression $field, string $letter): void
943*06a438b4SGreg Roach    {
944*06a438b4SGreg Roach        switch ($letter) {
945*06a438b4SGreg Roach            case 'C':
946*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'C%')->where($field, 'NOT LIKE', 'CS%');
947*06a438b4SGreg Roach                break;
948*06a438b4SGreg Roach
949*06a438b4SGreg Roach            case 'D':
950*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'D%')->where($field, 'NOT LIKE', 'DZ%');
951*06a438b4SGreg Roach                break;
952*06a438b4SGreg Roach
953*06a438b4SGreg Roach            case 'DZ':
954*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'DZ%')->where($field, 'NOT LIKE', 'DZS%');
955*06a438b4SGreg Roach                break;
956*06a438b4SGreg Roach
957*06a438b4SGreg Roach            case 'G':
958*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'G%')->where($field, 'NOT LIKE', 'GY%');
959*06a438b4SGreg Roach                break;
960*06a438b4SGreg Roach
961*06a438b4SGreg Roach            case 'L':
962*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'L%')->where($field, 'NOT LIKE', 'LY%');
963*06a438b4SGreg Roach                break;
964*06a438b4SGreg Roach
965*06a438b4SGreg Roach            case 'N':
966*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'N%')->where($field, 'NOT LIKE', 'NY%');
967*06a438b4SGreg Roach                break;
968*06a438b4SGreg Roach
969*06a438b4SGreg Roach            case 'S':
970*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'S%')->where($field, 'NOT LIKE', 'SZ%');
971*06a438b4SGreg Roach                break;
972*06a438b4SGreg Roach
973*06a438b4SGreg Roach            case 'T':
974*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'T%')->where($field, 'NOT LIKE', 'TY%');
975*06a438b4SGreg Roach                break;
976*06a438b4SGreg Roach
977*06a438b4SGreg Roach            case 'Z':
978*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'Z%')->where($field, 'NOT LIKE', 'ZS%');
979*06a438b4SGreg Roach                break;
980*06a438b4SGreg Roach
981*06a438b4SGreg Roach            default:
982*06a438b4SGreg Roach                $query->where($field, 'LIKE', '\\' . $letter . '%');
983*06a438b4SGreg Roach                break;
984*06a438b4SGreg Roach        }
985*06a438b4SGreg Roach    }
986*06a438b4SGreg Roach
987*06a438b4SGreg Roach    /**
988*06a438b4SGreg Roach     * In Norwegian and Danish, AA gets listed under Å, NOT A
989*06a438b4SGreg Roach     *
990*06a438b4SGreg Roach     * @param Builder    $query
991*06a438b4SGreg Roach     * @param Expression $field
992*06a438b4SGreg Roach     * @param string     $letter
993*06a438b4SGreg Roach     */
994*06a438b4SGreg Roach    protected function whereInitialNorwegian(Builder $query, Expression $field, string $letter): void
995*06a438b4SGreg Roach    {
996*06a438b4SGreg Roach        switch ($letter) {
997*06a438b4SGreg Roach            case 'A':
998*06a438b4SGreg Roach                $query->where($field, 'LIKE', 'A%')->where($field, 'NOT LIKE', 'AA%');
999*06a438b4SGreg Roach                break;
1000*06a438b4SGreg Roach
1001*06a438b4SGreg Roach            case 'Å':
1002*06a438b4SGreg Roach                $query->where(static function (Builder $query) use ($field): void {
1003*06a438b4SGreg Roach                    $query
1004*06a438b4SGreg Roach                        ->where($field, 'LIKE', 'Å%')
1005*06a438b4SGreg Roach                        ->orWhere($field, 'LIKE', 'AA%');
1006*06a438b4SGreg Roach                });
1007*06a438b4SGreg Roach                break;
1008*06a438b4SGreg Roach
1009*06a438b4SGreg Roach            default:
1010*06a438b4SGreg Roach                $query->where($field, 'LIKE', '\\' . $letter . '%');
1011*06a438b4SGreg Roach                break;
1012*06a438b4SGreg Roach        }
1013*06a438b4SGreg Roach    }
1014*06a438b4SGreg Roach
1015*06a438b4SGreg Roach    /**
1016*06a438b4SGreg Roach     * In Swedish and Finnish, AA gets listed under A, NOT Å (even though Swedish collation says they should).
1017*06a438b4SGreg Roach     *
1018*06a438b4SGreg Roach     * @param Builder    $query
1019*06a438b4SGreg Roach     * @param Expression $field
1020*06a438b4SGreg Roach     * @param string     $letter
1021*06a438b4SGreg Roach     */
1022*06a438b4SGreg Roach    protected function whereInitialSwedish(Builder $query, Expression $field, string $letter): void
1023*06a438b4SGreg Roach    {
1024*06a438b4SGreg Roach        if ($letter === 'Å') {
1025*06a438b4SGreg Roach            $query->where($field, 'LIKE', 'Å%')->where($field, 'NOT LIKE', 'AA%');
1026*06a438b4SGreg Roach        } else {
1027*06a438b4SGreg Roach            $query->where($field, 'LIKE', '\\' . $letter . '%');
1028*06a438b4SGreg Roach        }
1029*06a438b4SGreg Roach    }
103067992b6aSRichard Cissee}
1031