1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Http\RequestHandlers; 21 22use Fisharebest\Algorithm\ConnectedComponent; 23use Fisharebest\Webtrees\DB; 24use Fisharebest\Webtrees\Http\ViewResponseTrait; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Registry; 28use Fisharebest\Webtrees\Validator; 29use Illuminate\Support\Collection; 30use Psr\Http\Message\ResponseInterface; 31use Psr\Http\Message\ServerRequestInterface; 32use Psr\Http\Server\RequestHandlerInterface; 33 34use function count; 35use function in_array; 36use function strtolower; 37 38/** 39 * Find groups of unrelated individuals. 40 */ 41class UnconnectedPage implements RequestHandlerInterface 42{ 43 use ViewResponseTrait; 44 45 /** 46 * @param ServerRequestInterface $request 47 * 48 * @return ResponseInterface 49 */ 50 public function handle(ServerRequestInterface $request): ResponseInterface 51 { 52 $tree = Validator::attributes($request)->tree(); 53 $user = Validator::attributes($request)->user(); 54 $aliases = Validator::queryParams($request)->boolean('aliases', false); 55 $associates = Validator::queryParams($request)->boolean('associates', false); 56 57 // Connect individuals using these links. 58 $links = ['FAMS', 'FAMC']; 59 60 if ($aliases) { 61 $links[] = 'ALIA'; 62 } 63 64 if ($associates) { 65 $links[] = 'ASSO'; 66 $links[] = '_ASSO'; 67 } 68 69 $rows = DB::table('link') 70 ->where('l_file', '=', $tree->id()) 71 ->whereIn('l_type', $links) 72 ->select(['l_from', 'l_to']) 73 ->get(); 74 75 $graph = DB::table('individuals') 76 ->where('i_file', '=', $tree->id()) 77 ->pluck('i_id') 78 ->mapWithKeys(static function (string $xref): array { 79 return [$xref => []]; 80 }) 81 ->all(); 82 83 foreach ($rows as $row) { 84 $graph[$row->l_from][$row->l_to] = 1; 85 $graph[$row->l_to][$row->l_from] = 1; 86 } 87 88 $algorithm = new ConnectedComponent($graph); 89 $components = $algorithm->findConnectedComponents(); 90 $root = $tree->significantIndividual($user); 91 $xref = $root->xref(); 92 93 /** @var Individual[][] */ 94 $individual_groups = []; 95 96 foreach ($components as $component) { 97 // Allow for upper/lower-case mismatches, and all-numeric XREFs 98 $component = array_map(static fn ($x): string => strtolower((string) $x), $component); 99 100 if (!in_array(strtolower($xref), $component, true)) { 101 $individual_groups[] = DB::table('individuals') 102 ->where('i_file', '=', $tree->id()) 103 ->whereIn('i_id', $component) 104 ->get() 105 ->map(Registry::individualFactory()->mapper($tree)) 106 ->filter(); 107 } 108 } 109 110 usort($individual_groups, static fn (Collection $x, Collection $y): int => count($x) <=> count($y)); 111 112 $title = I18N::translate('Find unrelated individuals') . ' — ' . e($tree->title()); 113 114 $this->layout = 'layouts/administration'; 115 116 return $this->viewResponse('admin/trees-unconnected', [ 117 'aliases' => $aliases, 118 'associates' => $associates, 119 'root' => $root, 120 'individual_groups' => $individual_groups, 121 'title' => $title, 122 'tree' => $tree, 123 ]); 124 } 125} 126