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