1168ff6f3Sric2016<?php 23976b470SGreg Roach 3168ff6f3Sric2016/** 4168ff6f3Sric2016 * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify 7168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by 8168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or 9168ff6f3Sric2016 * (at your option) any later version. 10168ff6f3Sric2016 * This program is distributed in the hope that it will be useful, 11168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13168ff6f3Sric2016 * GNU General Public License for more details. 14168ff6f3Sric2016 * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16168ff6f3Sric2016 */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 21168ff6f3Sric2016 22185cbb4dSGreg Roachuse Closure; 233cfcc809SGreg Roachuse Fig\Http\Message\RequestMethodInterface; 249b5537c3SGreg Roachuse Fisharebest\Algorithm\Dijkstra; 25168ff6f3Sric2016use Fisharebest\Webtrees\Auth; 261fe542e9SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 276f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB; 2845ac604bSGreg Roachuse Fisharebest\Webtrees\FlashMessages; 296fcafd02SGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 30168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 31168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 321e3273c9SGreg Roachuse Fisharebest\Webtrees\Menu; 336fcafd02SGreg Roachuse Fisharebest\Webtrees\Registry; 346fcafd02SGreg Roachuse Fisharebest\Webtrees\Services\RelationshipService; 353df1e584SGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 3645ac604bSGreg Roachuse Fisharebest\Webtrees\Tree; 37b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 389b5537c3SGreg Roachuse Illuminate\Database\Query\JoinClause; 396fcafd02SGreg Roachuse Illuminate\Support\Collection; 406ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 416ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 423cfcc809SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 433976b470SGreg Roach 44c5c278deSGreg Roachuse function array_map; 45c5c278deSGreg Roachuse function asset; 4610e06497SGreg Roachuse function count; 47c5c278deSGreg Roachuse function current; 48c5c278deSGreg Roachuse function e; 49c5c278deSGreg Roachuse function implode; 5010e06497SGreg Roachuse function in_array; 51c5c278deSGreg Roachuse function max; 52c5c278deSGreg Roachuse function min; 53c5c278deSGreg Roachuse function next; 54c5c278deSGreg Roachuse function ob_get_clean; 55c5c278deSGreg Roachuse function ob_start; 56c5c278deSGreg Roachuse function preg_match; 573cfcc809SGreg Roachuse function redirect; 58c5c278deSGreg Roachuse function response; 593cfcc809SGreg Roachuse function route; 60c5c278deSGreg Roachuse function sort; 61f4ba05e3SGreg Roachuse function view; 62168ff6f3Sric2016 63168ff6f3Sric2016/** 64168ff6f3Sric2016 * Class RelationshipsChartModule 65168ff6f3Sric2016 */ 667a1b7425SGreg Roachclass RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, ModuleConfigInterface, RequestHandlerInterface 67c1010edaSGreg Roach{ 6849a243cbSGreg Roach use ModuleChartTrait; 6949a243cbSGreg Roach use ModuleConfigTrait; 7049a243cbSGreg Roach 7172f04adfSGreg Roach protected const ROUTE_URL = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}'; 723cfcc809SGreg Roach 731e3273c9SGreg Roach /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */ 7416d6367aSGreg Roach public const UNLIMITED_RECURSION = 99; 751e3273c9SGreg Roach 761e3273c9SGreg Roach /** By default new trees allow unlimited recursion */ 7716d6367aSGreg Roach public const DEFAULT_RECURSION = '99'; 7845ac604bSGreg Roach 79e0bd7dc9SGreg Roach /** By default new trees search for all relationships (not via ancestors) */ 8016d6367aSGreg Roach public const DEFAULT_ANCESTORS = '0'; 813cfcc809SGreg Roach public const DEFAULT_PARAMETERS = [ 823cfcc809SGreg Roach 'ancestors' => self::DEFAULT_ANCESTORS, 833cfcc809SGreg Roach 'recursion' => self::DEFAULT_RECURSION, 843cfcc809SGreg Roach ]; 853cfcc809SGreg Roach 866fcafd02SGreg Roach private TreeService $tree_service; 876fcafd02SGreg Roach 886fcafd02SGreg Roach private RelationshipService $relationship_service; 893df1e584SGreg Roach 903df1e584SGreg Roach /** 916fcafd02SGreg Roach * @param RelationshipService $relationship_service 923df1e584SGreg Roach * @param TreeService $tree_service 933df1e584SGreg Roach */ 94c5c278deSGreg Roach public function __construct(RelationshipService $relationship_service, TreeService $tree_service) 953df1e584SGreg Roach { 966fcafd02SGreg Roach $this->relationship_service = $relationship_service; 973df1e584SGreg Roach $this->tree_service = $tree_service; 983df1e584SGreg Roach } 993df1e584SGreg Roach 1003cfcc809SGreg Roach /** 1013cfcc809SGreg Roach * Initialization. 1023cfcc809SGreg Roach * 1039e18e23bSGreg Roach * @return void 1043cfcc809SGreg Roach */ 1059e18e23bSGreg Roach public function boot(): void 1063cfcc809SGreg Roach { 107158900c2SGreg Roach Registry::routeFactory()->routeMap() 10872f04adfSGreg Roach ->get(static::class, static::ROUTE_URL, $this) 1093cfcc809SGreg Roach ->allows(RequestMethodInterface::METHOD_POST) 1103cfcc809SGreg Roach ->tokens([ 1113cfcc809SGreg Roach 'ancestors' => '\d+', 1123cfcc809SGreg Roach 'recursion' => '\d+', 1133cfcc809SGreg Roach ]); 1143cfcc809SGreg Roach } 115e0bd7dc9SGreg Roach 116168ff6f3Sric2016 /** 117168ff6f3Sric2016 * A sentence describing what this module does. 118168ff6f3Sric2016 * 119168ff6f3Sric2016 * @return string 120168ff6f3Sric2016 */ 12149a243cbSGreg Roach public function description(): string 122c1010edaSGreg Roach { 123bbb76c12SGreg Roach /* I18N: Description of the “RelationshipsChart” module */ 124bbb76c12SGreg Roach return I18N::translate('A chart displaying relationships between two individuals.'); 125168ff6f3Sric2016 } 126168ff6f3Sric2016 127168ff6f3Sric2016 /** 1286ccdf4f0SGreg Roach * Return a menu item for this chart - for use in individual boxes. 1296ccdf4f0SGreg Roach * 1306ccdf4f0SGreg Roach * @param Individual $individual 1316ccdf4f0SGreg Roach * 1326ccdf4f0SGreg Roach * @return Menu|null 1336ccdf4f0SGreg Roach */ 1346ccdf4f0SGreg Roach public function chartBoxMenu(Individual $individual): ?Menu 1356ccdf4f0SGreg Roach { 1366ccdf4f0SGreg Roach return $this->chartMenu($individual); 1376ccdf4f0SGreg Roach } 1386ccdf4f0SGreg Roach 1396ccdf4f0SGreg Roach /** 140e6562982SGreg Roach * A main menu item for this chart. 141168ff6f3Sric2016 * 1428e69695bSGreg Roach * @param Individual $individual 1438e69695bSGreg Roach * 144e6562982SGreg Roach * @return Menu 145168ff6f3Sric2016 */ 146e6562982SGreg Roach public function chartMenu(Individual $individual): Menu 147c1010edaSGreg Roach { 148a0c3c04bSGreg Roach $my_xref = $individual->tree()->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF); 149168ff6f3Sric2016 150a0c3c04bSGreg Roach if ($my_xref !== '' && $my_xref !== $individual->xref()) { 151a0c3c04bSGreg Roach $my_record = Registry::individualFactory()->make($my_xref, $individual->tree()); 152a0c3c04bSGreg Roach 1532491533aSGreg Roach if ($my_record instanceof Individual) { 154168ff6f3Sric2016 return new Menu( 155168ff6f3Sric2016 I18N::translate('Relationship to me'), 156a0c3c04bSGreg Roach $this->chartUrl($my_record, ['xref2' => $individual->xref()]), 157377a2979SGreg Roach $this->chartMenuClass(), 158e6562982SGreg Roach $this->chartUrlAttributes() 159168ff6f3Sric2016 ); 160b2ce94c6SRico Sonntag } 1612491533aSGreg Roach } 162b2ce94c6SRico Sonntag 163168ff6f3Sric2016 return new Menu( 164e6562982SGreg Roach $this->title(), 165e6562982SGreg Roach $this->chartUrl($individual), 166377a2979SGreg Roach $this->chartMenuClass(), 167e6562982SGreg Roach $this->chartUrlAttributes() 168168ff6f3Sric2016 ); 169168ff6f3Sric2016 } 170168ff6f3Sric2016 1714eb71cfaSGreg Roach /** 172377a2979SGreg Roach * CSS class for the URL. 173377a2979SGreg Roach * 174377a2979SGreg Roach * @return string 175377a2979SGreg Roach */ 176377a2979SGreg Roach public function chartMenuClass(): string 177377a2979SGreg Roach { 178377a2979SGreg Roach return 'menu-chart-relationship'; 179377a2979SGreg Roach } 180377a2979SGreg Roach 181377a2979SGreg Roach /** 1826ccdf4f0SGreg Roach * How should this module be identified in the control panel, etc.? 1834eb71cfaSGreg Roach * 1846ccdf4f0SGreg Roach * @return string 1854eb71cfaSGreg Roach */ 1866ccdf4f0SGreg Roach public function title(): string 187c1010edaSGreg Roach { 1886ccdf4f0SGreg Roach /* I18N: Name of a module/chart */ 1896ccdf4f0SGreg Roach return I18N::translate('Relationships'); 190e6562982SGreg Roach } 191e6562982SGreg Roach 192e6562982SGreg Roach /** 1933cfcc809SGreg Roach * The URL for a page showing chart options. 19457ab2231SGreg Roach * 1953cfcc809SGreg Roach * @param Individual $individual 19676d39c55SGreg Roach * @param array<bool|int|string|array<string>|null> $parameters 19718d7a90dSGreg Roach * 1983cfcc809SGreg Roach * @return string 199e0bd7dc9SGreg Roach */ 2003cfcc809SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 201c1010edaSGreg Roach { 20272f04adfSGreg Roach return route(static::class, [ 2033cfcc809SGreg Roach 'xref' => $individual->xref(), 2043cfcc809SGreg Roach 'tree' => $individual->tree()->name(), 2053cfcc809SGreg Roach ] + $parameters + self::DEFAULT_PARAMETERS); 2061e3273c9SGreg Roach } 2079b5537c3SGreg Roach 2089b5537c3SGreg Roach /** 2096ccdf4f0SGreg Roach * @param ServerRequestInterface $request 2106ccdf4f0SGreg Roach * 2116ccdf4f0SGreg Roach * @return ResponseInterface 2126ccdf4f0SGreg Roach */ 2133cfcc809SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 2146ccdf4f0SGreg Roach { 215b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 216b55cbc6bSGreg Roach $xref = Validator::attributes($request)->isXref()->string('xref'); 217b55cbc6bSGreg Roach $xref2 = Validator::attributes($request)->isXref()->string('xref2', ''); 218b55cbc6bSGreg Roach $ajax = Validator::queryParams($request)->boolean('ajax', false); 2193cfcc809SGreg Roach $ancestors = (int) $request->getAttribute('ancestors'); 2203cfcc809SGreg Roach $recursion = (int) $request->getAttribute('recursion'); 221b55cbc6bSGreg Roach $user = Validator::attributes($request)->user(); 2229b5537c3SGreg Roach 2233cfcc809SGreg Roach // Convert POST requests into GET requests for pretty URLs. 2243cfcc809SGreg Roach if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 22572f04adfSGreg Roach return redirect(route(static::class, [ 2264ea62551SGreg Roach 'tree' => $tree->name(), 227b55cbc6bSGreg Roach 'ancestors' => Validator::parsedBody($request)->string('ancestors', ''), 228b55cbc6bSGreg Roach 'recursion' => Validator::parsedBody($request)->string('recursion', ''), 229b55cbc6bSGreg Roach 'xref' => Validator::parsedBody($request)->string('xref', ''), 230b55cbc6bSGreg Roach 'xref2' => Validator::parsedBody($request)->string('xref2', ''), 2313cfcc809SGreg Roach ])); 2323cfcc809SGreg Roach } 2339b5537c3SGreg Roach 2346b9cb339SGreg Roach $individual1 = Registry::individualFactory()->make($xref, $tree); 2356b9cb339SGreg Roach $individual2 = Registry::individualFactory()->make($xref2, $tree); 2369b5537c3SGreg Roach 2373dcc812bSGreg Roach $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS); 2383dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2399b5537c3SGreg Roach 2409b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 2419b5537c3SGreg Roach 242b55cbc6bSGreg Roach Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); 243b55cbc6bSGreg Roach 2443dcc812bSGreg Roach if ($individual1 instanceof Individual) { 2453c3fd0a5SGreg Roach $individual1 = Auth::checkIndividualAccess($individual1, false, true); 2463dcc812bSGreg Roach } 2473dcc812bSGreg Roach 2483dcc812bSGreg Roach if ($individual2 instanceof Individual) { 2493c3fd0a5SGreg Roach $individual2 = Auth::checkIndividualAccess($individual2, false, true); 2508365f910SGreg Roach } 2513dcc812bSGreg Roach 2529b5537c3SGreg Roach if ($individual1 instanceof Individual && $individual2 instanceof Individual) { 253b55cbc6bSGreg Roach if ($ajax) { 254f866a2aeSGreg Roach return $this->chart($individual1, $individual2, $recursion, $ancestors); 255f866a2aeSGreg Roach } 256f866a2aeSGreg Roach 2579b5537c3SGreg Roach /* I18N: %s are individual’s names */ 25839ca88baSGreg Roach $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName()); 2599b5537c3SGreg Roach $ajax_url = $this->chartUrl($individual1, [ 2609b5537c3SGreg Roach 'ajax' => true, 2619b5537c3SGreg Roach 'ancestors' => $ancestors, 2623cfcc809SGreg Roach 'recursion' => $recursion, 2633cfcc809SGreg Roach 'xref2' => $individual2->xref(), 2649b5537c3SGreg Roach ]); 2653dcc812bSGreg Roach } else { 2663dcc812bSGreg Roach $title = I18N::translate('Relationships'); 267f866a2aeSGreg Roach $ajax_url = ''; 2683dcc812bSGreg Roach } 2699b5537c3SGreg Roach 2709b5537c3SGreg Roach return $this->viewResponse('modules/relationships-chart/page', [ 2719b5537c3SGreg Roach 'ajax_url' => $ajax_url, 2729b5537c3SGreg Roach 'ancestors' => $ancestors, 2739b5537c3SGreg Roach 'ancestors_only' => $ancestors_only, 2749b5537c3SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 2759b5537c3SGreg Roach 'individual1' => $individual1, 2769b5537c3SGreg Roach 'individual2' => $individual2, 2779b5537c3SGreg Roach 'max_recursion' => $max_recursion, 27871378461SGreg Roach 'module' => $this->name(), 2799b5537c3SGreg Roach 'recursion' => $recursion, 2809b5537c3SGreg Roach 'recursion_options' => $this->recursionOptions($max_recursion), 2819b5537c3SGreg Roach 'title' => $title, 2823cfcc809SGreg Roach 'tree' => $tree, 2839b5537c3SGreg Roach ]); 2849b5537c3SGreg Roach } 2859b5537c3SGreg Roach 2869b5537c3SGreg Roach /** 2873dcc812bSGreg Roach * @param Individual $individual1 2883dcc812bSGreg Roach * @param Individual $individual2 2893dcc812bSGreg Roach * @param int $recursion 2903dcc812bSGreg Roach * @param int $ancestors 2919b5537c3SGreg Roach * 2926ccdf4f0SGreg Roach * @return ResponseInterface 2939b5537c3SGreg Roach */ 2946ccdf4f0SGreg Roach public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface 2959b5537c3SGreg Roach { 2963dcc812bSGreg Roach $tree = $individual1->tree(); 2979b5537c3SGreg Roach 2983dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2999b5537c3SGreg Roach 3009b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 3019b5537c3SGreg Roach 3029b5537c3SGreg Roach $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); 3039b5537c3SGreg Roach 3049b5537c3SGreg Roach ob_start(); 3059b5537c3SGreg Roach if (I18N::direction() === 'ltr') { 306e837ff07SGreg Roach $diagonal1 = asset('css/images/dline.png'); 307e837ff07SGreg Roach $diagonal2 = asset('css/images/dline2.png'); 3089b5537c3SGreg Roach } else { 309e837ff07SGreg Roach $diagonal1 = asset('css/images/dline2.png'); 310e837ff07SGreg Roach $diagonal2 = asset('css/images/dline.png'); 3119b5537c3SGreg Roach } 3129b5537c3SGreg Roach 3139b5537c3SGreg Roach $num_paths = 0; 3149b5537c3SGreg Roach foreach ($paths as $path) { 3159b5537c3SGreg Roach // Extract the relationship names between pairs of individuals 3169b5537c3SGreg Roach $relationships = $this->oldStyleRelationshipPath($tree, $path); 317075d1a05SGreg Roach if ($relationships === []) { 3189b5537c3SGreg Roach // Cannot see one of the families/individuals, due to privacy; 3199b5537c3SGreg Roach continue; 3209b5537c3SGreg Roach } 3216fcafd02SGreg Roach 3226fcafd02SGreg Roach $nodes = Collection::make($path) 3236fcafd02SGreg Roach ->map(static function (string $xref, int $key) use ($tree): GedcomRecord { 3246fcafd02SGreg Roach if ($key % 2 === 0) { 3256fcafd02SGreg Roach return Registry::individualFactory()->make($xref, $tree); 3266fcafd02SGreg Roach } 3276fcafd02SGreg Roach 3286fcafd02SGreg Roach return Registry::familyFactory()->make($xref, $tree); 3296fcafd02SGreg Roach }); 3306fcafd02SGreg Roach 331c5c278deSGreg Roach $relationship = $this->relationship_service->nameFromPath($nodes->all(), I18N::language()); 3326fcafd02SGreg Roach 333c5c278deSGreg Roach echo '<h3>', I18N::translate('Relationship: %s', $relationship), '</h3>'; 334c5c278deSGreg Roach 3359b5537c3SGreg Roach $num_paths++; 3369b5537c3SGreg Roach 3379b5537c3SGreg Roach // Use a table/grid for layout. 3389b5537c3SGreg Roach $table = []; 3399b5537c3SGreg Roach // Current position in the grid. 3409b5537c3SGreg Roach $x = 0; 3419b5537c3SGreg Roach $y = 0; 3429b5537c3SGreg Roach // Extent of the grid. 3439b5537c3SGreg Roach $min_y = 0; 3449b5537c3SGreg Roach $max_y = 0; 3459b5537c3SGreg Roach $max_x = 0; 3469b5537c3SGreg Roach // For each node in the path. 3479b5537c3SGreg Roach foreach ($path as $n => $xref) { 3489b5537c3SGreg Roach if ($n % 2 === 1) { 3499b5537c3SGreg Roach switch ($relationships[$n]) { 3509b5537c3SGreg Roach case 'hus': 3519b5537c3SGreg Roach case 'wif': 3529b5537c3SGreg Roach case 'spo': 3539b5537c3SGreg Roach case 'bro': 3549b5537c3SGreg Roach case 'sis': 3559b5537c3SGreg Roach case 'sib': 3569c259058SGreg Roach $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center; width: 94px; text-align: center"><div style="height: 32px;">' . $this->relationship_service->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px;">' . view('icons/arrow-right') . '</div></div>'; 3579b5537c3SGreg Roach $x += 2; 3589b5537c3SGreg Roach break; 3599b5537c3SGreg Roach case 'son': 3609b5537c3SGreg Roach case 'dau': 3619b5537c3SGreg Roach case 'chi': 3629b5537c3SGreg Roach if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { 363b55cbc6bSGreg Roach $table[$x + 1][$y - 1] = '<div style="background:url(' . $diagonal2 . '); width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: end;">' . $this->relationship_service->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: start;">' . view('icons/arrow-down') . '</div></div>'; 3649b5537c3SGreg Roach $x += 2; 3659b5537c3SGreg Roach } else { 3669c259058SGreg Roach $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div style="display: inline-block; width:50%; line-height: 64px;">' . $this->relationship_service->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width:50%; line-height: 64px;">' . view('icons/arrow-down') . '</div></div>'; 3679b5537c3SGreg Roach } 3689b5537c3SGreg Roach $y -= 2; 3699b5537c3SGreg Roach break; 3709b5537c3SGreg Roach case 'fat': 3719b5537c3SGreg Roach case 'mot': 3729b5537c3SGreg Roach case 'par': 3739b5537c3SGreg Roach if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { 374b55cbc6bSGreg Roach $table[$x + 1][$y + 1] = '<div style="background:url(' . $diagonal1 . '); background-position: top right; width: 64px; height: 64px; text-align: center;"><div style="height: 32px; text-align: start;">' . $this->relationship_service->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: end;">' . view('icons/arrow-down') . '</div></div>'; 3759b5537c3SGreg Roach $x += 2; 3769b5537c3SGreg Roach } else { 3779c259058SGreg Roach $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div style="display: inline-block; width: 50%; line-height: 64px;">' . $this->relationship_service->legacyNameAlgorithm($relationships[$n], Registry::individualFactory()->make($path[$n - 1], $tree), Registry::individualFactory()->make($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width: 50%; line-height: 32px">' . view('icons/arrow-up') . '</div></div>'; 3789b5537c3SGreg Roach } 3799b5537c3SGreg Roach $y += 2; 3809b5537c3SGreg Roach break; 3819b5537c3SGreg Roach } 3829b5537c3SGreg Roach $max_x = max($max_x, $x); 3839b5537c3SGreg Roach $min_y = min($min_y, $y); 3849b5537c3SGreg Roach $max_y = max($max_y, $y); 3859b5537c3SGreg Roach } else { 3866b9cb339SGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 387f4ba05e3SGreg Roach $table[$x][$y] = view('chart-box', ['individual' => $individual]); 3889b5537c3SGreg Roach } 3899b5537c3SGreg Roach } 3904a2590a5SGreg Roach echo '<div class="wt-chart wt-chart-relationships">'; 3919b5537c3SGreg Roach echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; 3929b5537c3SGreg Roach for ($y = $max_y; $y >= $min_y; --$y) { 3939b5537c3SGreg Roach echo '<tr>'; 3949b5537c3SGreg Roach for ($x = 0; $x <= $max_x; ++$x) { 3959b5537c3SGreg Roach echo '<td style="padding: 0;">'; 3969b5537c3SGreg Roach if (isset($table[$x][$y])) { 3979b5537c3SGreg Roach echo $table[$x][$y]; 3989b5537c3SGreg Roach } 3999b5537c3SGreg Roach echo '</td>'; 4009b5537c3SGreg Roach } 4019b5537c3SGreg Roach echo '</tr>'; 4029b5537c3SGreg Roach } 4039b5537c3SGreg Roach echo '</table>'; 4049b5537c3SGreg Roach echo '</div>'; 4059b5537c3SGreg Roach } 4069b5537c3SGreg Roach 4079b5537c3SGreg Roach if (!$num_paths) { 4089b5537c3SGreg Roach echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; 4099b5537c3SGreg Roach } 4109b5537c3SGreg Roach 4119b5537c3SGreg Roach $html = ob_get_clean(); 4129b5537c3SGreg Roach 4136ccdf4f0SGreg Roach return response($html); 4149b5537c3SGreg Roach } 4159b5537c3SGreg Roach 4169b5537c3SGreg Roach /** 4173cfcc809SGreg Roach * @param ServerRequestInterface $request 4183cfcc809SGreg Roach * 4193cfcc809SGreg Roach * @return ResponseInterface 4203cfcc809SGreg Roach */ 4213cfcc809SGreg Roach public function getAdminAction(ServerRequestInterface $request): ResponseInterface 4223cfcc809SGreg Roach { 4233cfcc809SGreg Roach $this->layout = 'layouts/administration'; 4243cfcc809SGreg Roach 4253cfcc809SGreg Roach return $this->viewResponse('modules/relationships-chart/config', [ 4263df1e584SGreg Roach 'all_trees' => $this->tree_service->all(), 4273cfcc809SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 4283cfcc809SGreg Roach 'default_ancestors' => self::DEFAULT_ANCESTORS, 4293cfcc809SGreg Roach 'default_recursion' => self::DEFAULT_RECURSION, 4303cfcc809SGreg Roach 'recursion_options' => $this->recursionConfigOptions(), 4313cfcc809SGreg Roach 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), 4323cfcc809SGreg Roach ]); 4333cfcc809SGreg Roach } 4343cfcc809SGreg Roach 4353cfcc809SGreg Roach /** 4363cfcc809SGreg Roach * @param ServerRequestInterface $request 4373cfcc809SGreg Roach * 4383cfcc809SGreg Roach * @return ResponseInterface 4393cfcc809SGreg Roach */ 4403cfcc809SGreg Roach public function postAdminAction(ServerRequestInterface $request): ResponseInterface 4413cfcc809SGreg Roach { 4423df1e584SGreg Roach foreach ($this->tree_service->all() as $tree) { 443748dbe15SGreg Roach $recursion = Validator::parsedBody($request)->integer('relationship-recursion-' . $tree->id()); 444748dbe15SGreg Roach $ancestors = Validator::parsedBody($request)->string('relationship-ancestors-' . $tree->id()); 4453cfcc809SGreg Roach 446748dbe15SGreg Roach $tree->setPreference('RELATIONSHIP_RECURSION', (string) $recursion); 4473cfcc809SGreg Roach $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors); 4483cfcc809SGreg Roach } 4493cfcc809SGreg Roach 4503cfcc809SGreg Roach FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success'); 4513cfcc809SGreg Roach 4523cfcc809SGreg Roach return redirect($this->getConfigLink()); 4533cfcc809SGreg Roach } 4543cfcc809SGreg Roach 4553cfcc809SGreg Roach /** 4563cfcc809SGreg Roach * Possible options for the ancestors option 4573cfcc809SGreg Roach * 4587c2c99faSGreg Roach * @return array<int,string> 4593cfcc809SGreg Roach */ 4603cfcc809SGreg Roach private function ancestorsOptions(): array 4613cfcc809SGreg Roach { 4623cfcc809SGreg Roach return [ 4633cfcc809SGreg Roach 0 => I18N::translate('Find any relationship'), 4643cfcc809SGreg Roach 1 => I18N::translate('Find relationships via ancestors'), 4653cfcc809SGreg Roach ]; 4663cfcc809SGreg Roach } 4673cfcc809SGreg Roach 4683cfcc809SGreg Roach /** 4693cfcc809SGreg Roach * Possible options for the recursion option 4703cfcc809SGreg Roach * 4717c2c99faSGreg Roach * @return array<int,string> 4723cfcc809SGreg Roach */ 4733cfcc809SGreg Roach private function recursionConfigOptions(): array 4743cfcc809SGreg Roach { 4753cfcc809SGreg Roach return [ 4763cfcc809SGreg Roach 0 => I18N::translate('none'), 4773cfcc809SGreg Roach 1 => I18N::number(1), 4783cfcc809SGreg Roach 2 => I18N::number(2), 4793cfcc809SGreg Roach 3 => I18N::number(3), 4803cfcc809SGreg Roach self::UNLIMITED_RECURSION => I18N::translate('unlimited'), 4813cfcc809SGreg Roach ]; 4823cfcc809SGreg Roach } 4833cfcc809SGreg Roach 4843cfcc809SGreg Roach /** 4859b5537c3SGreg Roach * Calculate the shortest paths - or all paths - between two individuals. 4869b5537c3SGreg Roach * 4879b5537c3SGreg Roach * @param Individual $individual1 4889b5537c3SGreg Roach * @param Individual $individual2 4899b5537c3SGreg Roach * @param int $recursion How many levels of recursion to use 4909b5537c3SGreg Roach * @param bool $ancestor Restrict to relationships via a common ancestor 4919b5537c3SGreg Roach * 49224f2a3afSGreg Roach * @return array<array<string>> 4939b5537c3SGreg Roach */ 49424f2a3afSGreg Roach private function calculateRelationships( 49524f2a3afSGreg Roach Individual $individual1, 49624f2a3afSGreg Roach Individual $individual2, 49724f2a3afSGreg Roach int $recursion, 49824f2a3afSGreg Roach bool $ancestor = false 49924f2a3afSGreg Roach ): array { 5003dcc812bSGreg Roach $tree = $individual1->tree(); 5013dcc812bSGreg Roach 5029b5537c3SGreg Roach $rows = DB::table('link') 5033dcc812bSGreg Roach ->where('l_file', '=', $tree->id()) 5049b5537c3SGreg Roach ->whereIn('l_type', ['FAMS', 'FAMC']) 5059b5537c3SGreg Roach ->select(['l_from', 'l_to']) 5069b5537c3SGreg Roach ->get(); 5079b5537c3SGreg Roach 5089b5537c3SGreg Roach // Optionally restrict the graph to the ancestors of the individuals. 5099b5537c3SGreg Roach if ($ancestor) { 5103dcc812bSGreg Roach $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id()); 5113dcc812bSGreg Roach $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id()); 5129b5537c3SGreg Roach } else { 5139b5537c3SGreg Roach $ancestors = []; 5149b5537c3SGreg Roach $exclude = []; 5159b5537c3SGreg Roach } 5169b5537c3SGreg Roach 5179b5537c3SGreg Roach $graph = []; 5189b5537c3SGreg Roach 5199b5537c3SGreg Roach foreach ($rows as $row) { 520075d1a05SGreg Roach if ($ancestors === [] || in_array($row->l_from, $ancestors, true) && !in_array($row->l_to, $exclude, true)) { 5219b5537c3SGreg Roach $graph[$row->l_from][$row->l_to] = 1; 5229b5537c3SGreg Roach $graph[$row->l_to][$row->l_from] = 1; 5239b5537c3SGreg Roach } 5249b5537c3SGreg Roach } 5259b5537c3SGreg Roach 5269b5537c3SGreg Roach $xref1 = $individual1->xref(); 5279b5537c3SGreg Roach $xref2 = $individual2->xref(); 5289b5537c3SGreg Roach $dijkstra = new Dijkstra($graph); 5299b5537c3SGreg Roach $paths = $dijkstra->shortestPaths($xref1, $xref2); 5309b5537c3SGreg Roach 5319b5537c3SGreg Roach // Only process each exclusion list once; 5329b5537c3SGreg Roach $excluded = []; 5339b5537c3SGreg Roach 5349b5537c3SGreg Roach $queue = []; 5359b5537c3SGreg Roach foreach ($paths as $path) { 5369b5537c3SGreg Roach // Insert the paths into the queue, with an exclusion list. 5379b5537c3SGreg Roach $queue[] = [ 5389b5537c3SGreg Roach 'path' => $path, 5399b5537c3SGreg Roach 'exclude' => [], 5409b5537c3SGreg Roach ]; 5419b5537c3SGreg Roach // While there are un-extended paths 5429b5537c3SGreg Roach for ($next = current($queue); $next !== false; $next = next($queue)) { 5439b5537c3SGreg Roach // For each family on the path 5449b5537c3SGreg Roach for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { 5459b5537c3SGreg Roach $exclude = $next['exclude']; 5469b5537c3SGreg Roach if (count($exclude) >= $recursion) { 5479b5537c3SGreg Roach continue; 5489b5537c3SGreg Roach } 5499b5537c3SGreg Roach $exclude[] = $next['path'][$n]; 5509b5537c3SGreg Roach sort($exclude); 5519b5537c3SGreg Roach $tmp = implode('-', $exclude); 55222d65e5aSGreg Roach if (in_array($tmp, $excluded, true)) { 5539b5537c3SGreg Roach continue; 5549b5537c3SGreg Roach } 5559b5537c3SGreg Roach 5569b5537c3SGreg Roach $excluded[] = $tmp; 5579b5537c3SGreg Roach // Add any new path to the queue 5589b5537c3SGreg Roach foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { 5599b5537c3SGreg Roach $queue[] = [ 5609b5537c3SGreg Roach 'path' => $new_path, 5619b5537c3SGreg Roach 'exclude' => $exclude, 5629b5537c3SGreg Roach ]; 5639b5537c3SGreg Roach } 5649b5537c3SGreg Roach } 5659b5537c3SGreg Roach } 5669b5537c3SGreg Roach } 567185cbb4dSGreg Roach // Extract the paths from the queue. 5689b5537c3SGreg Roach $paths = []; 5699b5537c3SGreg Roach foreach ($queue as $next) { 570185cbb4dSGreg Roach // The Dijkstra library does not use strict types, and converts 571185cbb4dSGreg Roach // numeric array keys (XREFs) from strings to integers; 572185cbb4dSGreg Roach $path = array_map($this->stringMapper(), $next['path']); 573185cbb4dSGreg Roach 574185cbb4dSGreg Roach // Remove duplicates 575185cbb4dSGreg Roach $paths[implode('-', $next['path'])] = $path; 5769b5537c3SGreg Roach } 5779b5537c3SGreg Roach 5789b5537c3SGreg Roach return $paths; 5799b5537c3SGreg Roach } 5809b5537c3SGreg Roach 5819b5537c3SGreg Roach /** 582185cbb4dSGreg Roach * Convert numeric values to strings 583185cbb4dSGreg Roach * 584c6921a17SGreg Roach * @return Closure(int|string):string 585185cbb4dSGreg Roach */ 586c3f3b628SGreg Roach private function stringMapper(): Closure 587c3f3b628SGreg Roach { 588*f25fc0f9SGreg Roach return static fn($xref) => (string) $xref; 589185cbb4dSGreg Roach } 590185cbb4dSGreg Roach 591185cbb4dSGreg Roach /** 5929b5537c3SGreg Roach * Find all ancestors of a list of individuals 5939b5537c3SGreg Roach * 5949b5537c3SGreg Roach * @param string $xref1 5959b5537c3SGreg Roach * @param string $xref2 5969b5537c3SGreg Roach * @param int $tree_id 5979b5537c3SGreg Roach * 59824f2a3afSGreg Roach * @return array<string> 5999b5537c3SGreg Roach */ 60024f2a3afSGreg Roach private function allAncestors(string $xref1, string $xref2, int $tree_id): array 6019b5537c3SGreg Roach { 6029b5537c3SGreg Roach $ancestors = [ 6039b5537c3SGreg Roach $xref1, 6049b5537c3SGreg Roach $xref2, 6059b5537c3SGreg Roach ]; 6069b5537c3SGreg Roach 6079b5537c3SGreg Roach $queue = [ 6089b5537c3SGreg Roach $xref1, 6099b5537c3SGreg Roach $xref2, 6109b5537c3SGreg Roach ]; 611075d1a05SGreg Roach while ($queue !== []) { 6129b5537c3SGreg Roach $parents = DB::table('link AS l1') 6130b5fd0a6SGreg Roach ->join('link AS l2', static function (JoinClause $join): void { 6149b5537c3SGreg Roach $join 6159b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 6169b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 6179b5537c3SGreg Roach }) 6189b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 6199b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMC') 6209b5537c3SGreg Roach ->where('l2.l_type', '=', 'FAMS') 6219b5537c3SGreg Roach ->whereIn('l1.l_from', $queue) 6229b5537c3SGreg Roach ->pluck('l2.l_from'); 6239b5537c3SGreg Roach 6249b5537c3SGreg Roach $queue = []; 6259b5537c3SGreg Roach foreach ($parents as $parent) { 62622d65e5aSGreg Roach if (!in_array($parent, $ancestors, true)) { 6279b5537c3SGreg Roach $ancestors[] = $parent; 6289b5537c3SGreg Roach $queue[] = $parent; 6299b5537c3SGreg Roach } 6309b5537c3SGreg Roach } 6319b5537c3SGreg Roach } 6329b5537c3SGreg Roach 6339b5537c3SGreg Roach return $ancestors; 6349b5537c3SGreg Roach } 6359b5537c3SGreg Roach 6369b5537c3SGreg Roach /** 6379b5537c3SGreg Roach * Find all families of two individuals 6389b5537c3SGreg Roach * 6399b5537c3SGreg Roach * @param string $xref1 6409b5537c3SGreg Roach * @param string $xref2 6419b5537c3SGreg Roach * @param int $tree_id 6429b5537c3SGreg Roach * 64324f2a3afSGreg Roach * @return array<string> 6449b5537c3SGreg Roach */ 64524f2a3afSGreg Roach private function excludeFamilies(string $xref1, string $xref2, int $tree_id): array 6469b5537c3SGreg Roach { 6479b5537c3SGreg Roach return DB::table('link AS l1') 6480b5fd0a6SGreg Roach ->join('link AS l2', static function (JoinClause $join): void { 6499b5537c3SGreg Roach $join 6509b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 6519b5537c3SGreg Roach ->on('l1.l_type', '=', 'l2.l_type') 6529b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 6539b5537c3SGreg Roach }) 6549b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 6559b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMS') 6569b5537c3SGreg Roach ->where('l1.l_from', '=', $xref1) 6579b5537c3SGreg Roach ->where('l2.l_from', '=', $xref2) 6589b5537c3SGreg Roach ->pluck('l1.l_to') 6599b5537c3SGreg Roach ->all(); 6609b5537c3SGreg Roach } 6619b5537c3SGreg Roach 6629b5537c3SGreg Roach /** 6636ccdf4f0SGreg Roach * Convert a path (list of XREFs) to an "old-style" string of relationships. 6646ccdf4f0SGreg Roach * Return an empty array, if privacy rules prevent us viewing any node. 6656ccdf4f0SGreg Roach * 6666ccdf4f0SGreg Roach * @param Tree $tree 66709482a55SGreg Roach * @param array<string> $path Alternately Individual / Family 6686ccdf4f0SGreg Roach * 66924f2a3afSGreg Roach * @return array<string> 6706ccdf4f0SGreg Roach */ 6716ccdf4f0SGreg Roach private function oldStyleRelationshipPath(Tree $tree, array $path): array 6726ccdf4f0SGreg Roach { 6736ccdf4f0SGreg Roach $spouse_codes = [ 6746ccdf4f0SGreg Roach 'M' => 'hus', 6756ccdf4f0SGreg Roach 'F' => 'wif', 6766ccdf4f0SGreg Roach 'U' => 'spo', 6776ccdf4f0SGreg Roach ]; 6786ccdf4f0SGreg Roach $parent_codes = [ 6796ccdf4f0SGreg Roach 'M' => 'fat', 6806ccdf4f0SGreg Roach 'F' => 'mot', 6816ccdf4f0SGreg Roach 'U' => 'par', 6826ccdf4f0SGreg Roach ]; 6836ccdf4f0SGreg Roach $child_codes = [ 6846ccdf4f0SGreg Roach 'M' => 'son', 6856ccdf4f0SGreg Roach 'F' => 'dau', 6866ccdf4f0SGreg Roach 'U' => 'chi', 6876ccdf4f0SGreg Roach ]; 6886ccdf4f0SGreg Roach $sibling_codes = [ 6896ccdf4f0SGreg Roach 'M' => 'bro', 6906ccdf4f0SGreg Roach 'F' => 'sis', 6916ccdf4f0SGreg Roach 'U' => 'sib', 6926ccdf4f0SGreg Roach ]; 6936ccdf4f0SGreg Roach $relationships = []; 6946ccdf4f0SGreg Roach 6956ccdf4f0SGreg Roach for ($i = 1, $count = count($path); $i < $count; $i += 2) { 6966b9cb339SGreg Roach $family = Registry::familyFactory()->make($path[$i], $tree); 6976b9cb339SGreg Roach $prev = Registry::individualFactory()->make($path[$i - 1], $tree); 6986b9cb339SGreg Roach $next = Registry::individualFactory()->make($path[$i + 1], $tree); 6996ccdf4f0SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { 7006ccdf4f0SGreg Roach $rel1 = $match[1]; 7016ccdf4f0SGreg Roach } else { 7026ccdf4f0SGreg Roach return []; 7036ccdf4f0SGreg Roach } 7046ccdf4f0SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { 7056ccdf4f0SGreg Roach $rel2 = $match[1]; 7066ccdf4f0SGreg Roach } else { 7076ccdf4f0SGreg Roach return []; 7086ccdf4f0SGreg Roach } 7096ccdf4f0SGreg Roach if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 71023a98013SGreg Roach $relationships[$i] = $spouse_codes[$next->sex()] ?? $spouse_codes['U']; 7116ccdf4f0SGreg Roach } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { 71223a98013SGreg Roach $relationships[$i] = $child_codes[$next->sex()] ?? $child_codes['U']; 7136ccdf4f0SGreg Roach } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 71423a98013SGreg Roach $relationships[$i] = $parent_codes[$next->sex()] ?? $parent_codes['U']; 7156ccdf4f0SGreg Roach } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { 71623a98013SGreg Roach $relationships[$i] = $sibling_codes[$next->sex()] ?? $sibling_codes['U']; 7176ccdf4f0SGreg Roach } 7186ccdf4f0SGreg Roach } 7196ccdf4f0SGreg Roach 7206ccdf4f0SGreg Roach return $relationships; 7216ccdf4f0SGreg Roach } 7226ccdf4f0SGreg Roach 7236ccdf4f0SGreg Roach /** 7249b5537c3SGreg Roach * Possible options for the recursion option 7259b5537c3SGreg Roach * 7269b5537c3SGreg Roach * @param int $max_recursion 7279b5537c3SGreg Roach * 72824f2a3afSGreg Roach * @return array<string> 7299b5537c3SGreg Roach */ 7309b5537c3SGreg Roach private function recursionOptions(int $max_recursion): array 7319b5537c3SGreg Roach { 7323dcc812bSGreg Roach if ($max_recursion === static::UNLIMITED_RECURSION) { 7339b5537c3SGreg Roach $text = I18N::translate('Find all possible relationships'); 7349b5537c3SGreg Roach } else { 7359b5537c3SGreg Roach $text = I18N::translate('Find other relationships'); 7369b5537c3SGreg Roach } 7379b5537c3SGreg Roach 7389b5537c3SGreg Roach return [ 7399b5537c3SGreg Roach '0' => I18N::translate('Find the closest relationships'), 7409b5537c3SGreg Roach $max_recursion => $text, 7419b5537c3SGreg Roach ]; 7429b5537c3SGreg Roach } 743168ff6f3Sric2016} 744