1168ff6f3Sric2016<?php 23976b470SGreg Roach 3168ff6f3Sric2016/** 4168ff6f3Sric2016 * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 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 15168ff6f3Sric2016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16168ff6f3Sric2016 */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 21168ff6f3Sric2016 223cfcc809SGreg Roachuse Aura\Router\RouterContainer; 23185cbb4dSGreg Roachuse Closure; 243cfcc809SGreg Roachuse Fig\Http\Message\RequestMethodInterface; 259b5537c3SGreg Roachuse Fisharebest\Algorithm\Dijkstra; 26168ff6f3Sric2016use Fisharebest\Webtrees\Auth; 279b5537c3SGreg Roachuse Fisharebest\Webtrees\Family; 2845ac604bSGreg Roachuse Fisharebest\Webtrees\FlashMessages; 299b5537c3SGreg Roachuse Fisharebest\Webtrees\Functions\Functions; 30168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 31168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 321e3273c9SGreg Roachuse Fisharebest\Webtrees\Menu; 3345ac604bSGreg Roachuse Fisharebest\Webtrees\Tree; 349b5537c3SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 359b5537c3SGreg Roachuse Illuminate\Database\Query\JoinClause; 366ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 376ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 383cfcc809SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 393976b470SGreg Roach 403cfcc809SGreg Roachuse function redirect; 413cfcc809SGreg Roachuse function route; 42f4ba05e3SGreg Roachuse function view; 43168ff6f3Sric2016 44168ff6f3Sric2016/** 45168ff6f3Sric2016 * Class RelationshipsChartModule 46168ff6f3Sric2016 */ 473cfcc809SGreg Roachclass RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface 48c1010edaSGreg Roach{ 4949a243cbSGreg Roach use ModuleChartTrait; 5049a243cbSGreg Roach use ModuleConfigTrait; 5149a243cbSGreg Roach 523cfcc809SGreg Roach private const ROUTE_NAME = 'relationships'; 533cfcc809SGreg Roach private const ROUTE_URL = '/tree/{tree}/relationships-{ancestors}-{recursion}/{xref}{/xref2}'; 543cfcc809SGreg Roach 551e3273c9SGreg Roach /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */ 5616d6367aSGreg Roach public const UNLIMITED_RECURSION = 99; 571e3273c9SGreg Roach 581e3273c9SGreg Roach /** By default new trees allow unlimited recursion */ 5916d6367aSGreg Roach public const DEFAULT_RECURSION = '99'; 6045ac604bSGreg Roach 61e0bd7dc9SGreg Roach /** By default new trees search for all relationships (not via ancestors) */ 6216d6367aSGreg Roach public const DEFAULT_ANCESTORS = '0'; 633cfcc809SGreg Roach public const DEFAULT_PARAMETERS = [ 643cfcc809SGreg Roach 'ancestors' => self::DEFAULT_ANCESTORS, 653cfcc809SGreg Roach 'recursion' => self::DEFAULT_RECURSION, 663cfcc809SGreg Roach ]; 673cfcc809SGreg Roach 683cfcc809SGreg Roach /** 693cfcc809SGreg Roach * Initialization. 703cfcc809SGreg Roach * 713cfcc809SGreg Roach * @param RouterContainer $router_container 723cfcc809SGreg Roach */ 733cfcc809SGreg Roach public function boot(RouterContainer $router_container) 743cfcc809SGreg Roach { 753cfcc809SGreg Roach $router_container->getMap() 763cfcc809SGreg Roach ->get(self::ROUTE_NAME, self::ROUTE_URL, self::class) 773cfcc809SGreg Roach ->allows(RequestMethodInterface::METHOD_POST) 783cfcc809SGreg Roach ->tokens([ 793cfcc809SGreg Roach 'ancestors' => '\d+', 803cfcc809SGreg Roach 'recursion' => '\d+', 813cfcc809SGreg Roach ])->defaults([ 823cfcc809SGreg Roach 'xref2' => '', 833cfcc809SGreg Roach ]); 843cfcc809SGreg Roach } 85e0bd7dc9SGreg Roach 86168ff6f3Sric2016 /** 87168ff6f3Sric2016 * A sentence describing what this module does. 88168ff6f3Sric2016 * 89168ff6f3Sric2016 * @return string 90168ff6f3Sric2016 */ 9149a243cbSGreg Roach public function description(): string 92c1010edaSGreg Roach { 93bbb76c12SGreg Roach /* I18N: Description of the “RelationshipsChart” module */ 94bbb76c12SGreg Roach return I18N::translate('A chart displaying relationships between two individuals.'); 95168ff6f3Sric2016 } 96168ff6f3Sric2016 97168ff6f3Sric2016 /** 986ccdf4f0SGreg Roach * Return a menu item for this chart - for use in individual boxes. 996ccdf4f0SGreg Roach * 1006ccdf4f0SGreg Roach * @param Individual $individual 1016ccdf4f0SGreg Roach * 1026ccdf4f0SGreg Roach * @return Menu|null 1036ccdf4f0SGreg Roach */ 1046ccdf4f0SGreg Roach public function chartBoxMenu(Individual $individual): ?Menu 1056ccdf4f0SGreg Roach { 1066ccdf4f0SGreg Roach return $this->chartMenu($individual); 1076ccdf4f0SGreg Roach } 1086ccdf4f0SGreg Roach 1096ccdf4f0SGreg Roach /** 110e6562982SGreg Roach * A main menu item for this chart. 111168ff6f3Sric2016 * 1128e69695bSGreg Roach * @param Individual $individual 1138e69695bSGreg Roach * 114e6562982SGreg Roach * @return Menu 115168ff6f3Sric2016 */ 116e6562982SGreg Roach public function chartMenu(Individual $individual): Menu 117c1010edaSGreg Roach { 118e6562982SGreg Roach $gedcomid = $individual->tree()->getUserPreference(Auth::user(), 'gedcomid'); 119168ff6f3Sric2016 1203dcc812bSGreg Roach if ($gedcomid !== '' && $gedcomid !== $individual->xref()) { 121168ff6f3Sric2016 return new Menu( 122168ff6f3Sric2016 I18N::translate('Relationship to me'), 123e6562982SGreg Roach $this->chartUrl($individual, ['xref2' => $gedcomid]), 124377a2979SGreg Roach $this->chartMenuClass(), 125e6562982SGreg Roach $this->chartUrlAttributes() 126168ff6f3Sric2016 ); 127b2ce94c6SRico Sonntag } 128b2ce94c6SRico Sonntag 129168ff6f3Sric2016 return new Menu( 130e6562982SGreg Roach $this->title(), 131e6562982SGreg Roach $this->chartUrl($individual), 132377a2979SGreg Roach $this->chartMenuClass(), 133e6562982SGreg Roach $this->chartUrlAttributes() 134168ff6f3Sric2016 ); 135168ff6f3Sric2016 } 136168ff6f3Sric2016 1374eb71cfaSGreg Roach /** 138377a2979SGreg Roach * CSS class for the URL. 139377a2979SGreg Roach * 140377a2979SGreg Roach * @return string 141377a2979SGreg Roach */ 142377a2979SGreg Roach public function chartMenuClass(): string 143377a2979SGreg Roach { 144377a2979SGreg Roach return 'menu-chart-relationship'; 145377a2979SGreg Roach } 146377a2979SGreg Roach 147377a2979SGreg Roach /** 1486ccdf4f0SGreg Roach * How should this module be identified in the control panel, etc.? 1494eb71cfaSGreg Roach * 1506ccdf4f0SGreg Roach * @return string 1514eb71cfaSGreg Roach */ 1526ccdf4f0SGreg Roach public function title(): string 153c1010edaSGreg Roach { 1546ccdf4f0SGreg Roach /* I18N: Name of a module/chart */ 1556ccdf4f0SGreg Roach return I18N::translate('Relationships'); 156e6562982SGreg Roach } 157e6562982SGreg Roach 158e6562982SGreg Roach /** 1593cfcc809SGreg Roach * The URL for a page showing chart options. 16057ab2231SGreg Roach * 1613cfcc809SGreg Roach * @param Individual $individual 162*59597b37SGreg Roach * @param mixed[] $parameters 16318d7a90dSGreg Roach * 1643cfcc809SGreg Roach * @return string 165e0bd7dc9SGreg Roach */ 1663cfcc809SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 167c1010edaSGreg Roach { 1683cfcc809SGreg Roach return route(self::ROUTE_NAME, [ 1693cfcc809SGreg Roach 'xref' => $individual->xref(), 1703cfcc809SGreg Roach 'tree' => $individual->tree()->name(), 1713cfcc809SGreg Roach ] + $parameters + self::DEFAULT_PARAMETERS); 1721e3273c9SGreg Roach } 1739b5537c3SGreg Roach 1749b5537c3SGreg Roach /** 1756ccdf4f0SGreg Roach * @param ServerRequestInterface $request 1766ccdf4f0SGreg Roach * 1776ccdf4f0SGreg Roach * @return ResponseInterface 1786ccdf4f0SGreg Roach */ 1793cfcc809SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 1806ccdf4f0SGreg Roach { 1813cfcc809SGreg Roach $ajax = $request->getQueryParams()['ajax'] ?? ''; 1823cfcc809SGreg Roach $ancestors = (int) $request->getAttribute('ancestors'); 1833cfcc809SGreg Roach $recursion = (int) $request->getAttribute('recursion'); 18457ab2231SGreg Roach $tree = $request->getAttribute('tree'); 18557ab2231SGreg Roach $user = $request->getAttribute('user'); 1863cfcc809SGreg Roach $xref = $request->getAttribute('xref'); 1873cfcc809SGreg Roach $xref2 = $request->getAttribute('xref2'); 1889b5537c3SGreg Roach 1893cfcc809SGreg Roach // Convert POST requests into GET requests for pretty URLs. 1903cfcc809SGreg Roach if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 1913cfcc809SGreg Roach return redirect(route(self::ROUTE_NAME, [ 1923cfcc809SGreg Roach 'ancestors' => $request->getParsedBody()['ancestors'], 1933cfcc809SGreg Roach 'recursion' => $request->getParsedBody()['recursion'], 1943cfcc809SGreg Roach 'tree' => $request->getAttribute('tree')->name(), 1953cfcc809SGreg Roach 'xref' => $request->getParsedBody()['xref'], 1963cfcc809SGreg Roach 'xref2' => $request->getParsedBody()['xref2'], 1973cfcc809SGreg Roach ])); 1983cfcc809SGreg Roach } 1999b5537c3SGreg Roach 2003dcc812bSGreg Roach $individual1 = Individual::getInstance($xref, $tree); 2019b5537c3SGreg Roach $individual2 = Individual::getInstance($xref2, $tree); 2029b5537c3SGreg Roach 2033dcc812bSGreg Roach $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS); 2043dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2059b5537c3SGreg Roach 2069b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 2079b5537c3SGreg Roach 2088365f910SGreg Roach if ($tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS') !== '1') { 2093dcc812bSGreg Roach if ($individual1 instanceof Individual) { 2103dcc812bSGreg Roach Auth::checkIndividualAccess($individual1); 2113dcc812bSGreg Roach } 2123dcc812bSGreg Roach 2133dcc812bSGreg Roach if ($individual2 instanceof Individual) { 2143dcc812bSGreg Roach Auth::checkIndividualAccess($individual2); 2153dcc812bSGreg Roach } 2168365f910SGreg Roach } 2173dcc812bSGreg Roach 2189867b2f0SGreg Roach Auth::checkComponentAccess($this, 'chart', $tree, $user); 2199867b2f0SGreg Roach 2209b5537c3SGreg Roach if ($individual1 instanceof Individual && $individual2 instanceof Individual) { 2210b93976aSGreg Roach if ($ajax === '1') { 222f866a2aeSGreg Roach return $this->chart($individual1, $individual2, $recursion, $ancestors); 223f866a2aeSGreg Roach } 224f866a2aeSGreg Roach 2259b5537c3SGreg Roach /* I18N: %s are individual’s names */ 22639ca88baSGreg Roach $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->fullName(), $individual2->fullName()); 2279b5537c3SGreg Roach $ajax_url = $this->chartUrl($individual1, [ 2289b5537c3SGreg Roach 'ajax' => true, 2299b5537c3SGreg Roach 'ancestors' => $ancestors, 2303cfcc809SGreg Roach 'recursion' => $recursion, 2313cfcc809SGreg Roach 'xref2' => $individual2->xref(), 2329b5537c3SGreg Roach ]); 2333dcc812bSGreg Roach } else { 2343dcc812bSGreg Roach $title = I18N::translate('Relationships'); 235f866a2aeSGreg Roach $ajax_url = ''; 2363dcc812bSGreg Roach } 2379b5537c3SGreg Roach 2389b5537c3SGreg Roach return $this->viewResponse('modules/relationships-chart/page', [ 2399b5537c3SGreg Roach 'ajax_url' => $ajax_url, 2409b5537c3SGreg Roach 'ancestors' => $ancestors, 2419b5537c3SGreg Roach 'ancestors_only' => $ancestors_only, 2429b5537c3SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 2439b5537c3SGreg Roach 'individual1' => $individual1, 2449b5537c3SGreg Roach 'individual2' => $individual2, 2459b5537c3SGreg Roach 'max_recursion' => $max_recursion, 24671378461SGreg Roach 'module' => $this->name(), 2479b5537c3SGreg Roach 'recursion' => $recursion, 2489b5537c3SGreg Roach 'recursion_options' => $this->recursionOptions($max_recursion), 2499b5537c3SGreg Roach 'title' => $title, 2503cfcc809SGreg Roach 'tree' => $tree, 2519b5537c3SGreg Roach ]); 2529b5537c3SGreg Roach } 2539b5537c3SGreg Roach 2549b5537c3SGreg Roach /** 2553dcc812bSGreg Roach * @param Individual $individual1 2563dcc812bSGreg Roach * @param Individual $individual2 2573dcc812bSGreg Roach * @param int $recursion 2583dcc812bSGreg Roach * @param int $ancestors 2599b5537c3SGreg Roach * 2606ccdf4f0SGreg Roach * @return ResponseInterface 2619b5537c3SGreg Roach */ 2626ccdf4f0SGreg Roach public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): ResponseInterface 2639b5537c3SGreg Roach { 2643dcc812bSGreg Roach $tree = $individual1->tree(); 2659b5537c3SGreg Roach 2663dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2679b5537c3SGreg Roach 2689b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 2699b5537c3SGreg Roach 2709b5537c3SGreg Roach $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); 2719b5537c3SGreg Roach 2729b5537c3SGreg Roach // @TODO - convert to views 2739b5537c3SGreg Roach ob_start(); 2749b5537c3SGreg Roach if (I18N::direction() === 'ltr') { 275e837ff07SGreg Roach $diagonal1 = asset('css/images/dline.png'); 276e837ff07SGreg Roach $diagonal2 = asset('css/images/dline2.png'); 2779b5537c3SGreg Roach } else { 278e837ff07SGreg Roach $diagonal1 = asset('css/images/dline2.png'); 279e837ff07SGreg Roach $diagonal2 = asset('css/images/dline.png'); 2809b5537c3SGreg Roach } 2819b5537c3SGreg Roach 2829b5537c3SGreg Roach $num_paths = 0; 2839b5537c3SGreg Roach foreach ($paths as $path) { 2849b5537c3SGreg Roach // Extract the relationship names between pairs of individuals 2859b5537c3SGreg Roach $relationships = $this->oldStyleRelationshipPath($tree, $path); 2869b5537c3SGreg Roach if (empty($relationships)) { 2879b5537c3SGreg Roach // Cannot see one of the families/individuals, due to privacy; 2889b5537c3SGreg Roach continue; 2899b5537c3SGreg Roach } 2909b5537c3SGreg Roach echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>'; 2919b5537c3SGreg Roach $num_paths++; 2929b5537c3SGreg Roach 2939b5537c3SGreg Roach // Use a table/grid for layout. 2949b5537c3SGreg Roach $table = []; 2959b5537c3SGreg Roach // Current position in the grid. 2969b5537c3SGreg Roach $x = 0; 2979b5537c3SGreg Roach $y = 0; 2989b5537c3SGreg Roach // Extent of the grid. 2999b5537c3SGreg Roach $min_y = 0; 3009b5537c3SGreg Roach $max_y = 0; 3019b5537c3SGreg Roach $max_x = 0; 3029b5537c3SGreg Roach // For each node in the path. 3039b5537c3SGreg Roach foreach ($path as $n => $xref) { 3049b5537c3SGreg Roach if ($n % 2 === 1) { 3059b5537c3SGreg Roach switch ($relationships[$n]) { 3069b5537c3SGreg Roach case 'hus': 3079b5537c3SGreg Roach case 'wif': 3089b5537c3SGreg Roach case 'spo': 3099b5537c3SGreg Roach case 'bro': 3109b5537c3SGreg Roach case 'sis': 3119b5537c3SGreg Roach case 'sib': 31239b853a7SGreg Roach $table[$x + 1][$y] = '<div style="background:url(' . e(asset('css/images/hline.png')) . ') repeat-x center; width: 94px; text-align: center"><div class="hline-text" style="height: 32px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Individual::getInstance($path[$n - 1], $tree), Individual::getInstance($path[$n + 1], $tree)) . '</div><div style="height: 32px;">' . view('icons/arrow-right') . '</div></div>'; 3139b5537c3SGreg Roach $x += 2; 3149b5537c3SGreg Roach break; 3159b5537c3SGreg Roach case 'son': 3169b5537c3SGreg Roach case 'dau': 3179b5537c3SGreg Roach case 'chi': 3189b5537c3SGreg Roach if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { 319d993d560SGreg 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;">' . Functions::getRelationshipNameFromPath($relationships[$n], Individual::getInstance($path[$n - 1], $tree), Individual::getInstance($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: start;">' . view('icons/arrow-down') . '</div></div>'; 3209b5537c3SGreg Roach $x += 2; 3219b5537c3SGreg Roach } else { 3224ac9dd10SGreg Roach $table[$x][$y - 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align: center;"><div class="vline-text" style="display: inline-block; width:50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Individual::getInstance($path[$n - 1], $tree), Individual::getInstance($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width:50%; line-height: 64px;">' . view('icons/arrow-down') . '</div></div>'; 3239b5537c3SGreg Roach } 3249b5537c3SGreg Roach $y -= 2; 3259b5537c3SGreg Roach break; 3269b5537c3SGreg Roach case 'fat': 3279b5537c3SGreg Roach case 'mot': 3289b5537c3SGreg Roach case 'par': 3299b5537c3SGreg Roach if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { 330d993d560SGreg 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;">' . Functions::getRelationshipNameFromPath($relationships[$n], Individual::getInstance($path[$n - 1], $tree), Individual::getInstance($path[$n + 1], $tree)) . '</div><div style="height: 32px; text-align: end;">' . view('icons/arrow-down') . '</div></div>'; 3319b5537c3SGreg Roach $x += 2; 3329b5537c3SGreg Roach } else { 3334ac9dd10SGreg Roach $table[$x][$y + 1] = '<div style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 64px;">' . Functions::getRelationshipNameFromPath($relationships[$n], Individual::getInstance($path[$n - 1], $tree), Individual::getInstance($path[$n + 1], $tree)) . '</div><div style="display: inline-block; width: 50%; line-height: 32px">' . view('icons/arrow-up') . '</div></div>'; 3349b5537c3SGreg Roach } 3359b5537c3SGreg Roach $y += 2; 3369b5537c3SGreg Roach break; 3379b5537c3SGreg Roach } 3389b5537c3SGreg Roach $max_x = max($max_x, $x); 3399b5537c3SGreg Roach $min_y = min($min_y, $y); 3409b5537c3SGreg Roach $max_y = max($max_y, $y); 3419b5537c3SGreg Roach } else { 3429b5537c3SGreg Roach $individual = Individual::getInstance($xref, $tree); 343f4ba05e3SGreg Roach $table[$x][$y] = view('chart-box', ['individual' => $individual]); 3449b5537c3SGreg Roach } 3459b5537c3SGreg Roach } 3464a2590a5SGreg Roach echo '<div class="wt-chart wt-chart-relationships">'; 3479b5537c3SGreg Roach echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; 3489b5537c3SGreg Roach for ($y = $max_y; $y >= $min_y; --$y) { 3499b5537c3SGreg Roach echo '<tr>'; 3509b5537c3SGreg Roach for ($x = 0; $x <= $max_x; ++$x) { 3519b5537c3SGreg Roach echo '<td style="padding: 0;">'; 3529b5537c3SGreg Roach if (isset($table[$x][$y])) { 3539b5537c3SGreg Roach echo $table[$x][$y]; 3549b5537c3SGreg Roach } 3559b5537c3SGreg Roach echo '</td>'; 3569b5537c3SGreg Roach } 3579b5537c3SGreg Roach echo '</tr>'; 3589b5537c3SGreg Roach } 3599b5537c3SGreg Roach echo '</table>'; 3609b5537c3SGreg Roach echo '</div>'; 3619b5537c3SGreg Roach } 3629b5537c3SGreg Roach 3639b5537c3SGreg Roach if (!$num_paths) { 3649b5537c3SGreg Roach echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; 3659b5537c3SGreg Roach } 3669b5537c3SGreg Roach 3679b5537c3SGreg Roach $html = ob_get_clean(); 3689b5537c3SGreg Roach 3696ccdf4f0SGreg Roach return response($html); 3709b5537c3SGreg Roach } 3719b5537c3SGreg Roach 3729b5537c3SGreg Roach /** 3733cfcc809SGreg Roach * @param ServerRequestInterface $request 3743cfcc809SGreg Roach * 3753cfcc809SGreg Roach * @return ResponseInterface 3763cfcc809SGreg Roach */ 3773cfcc809SGreg Roach public function getAdminAction(ServerRequestInterface $request): ResponseInterface 3783cfcc809SGreg Roach { 3793cfcc809SGreg Roach $this->layout = 'layouts/administration'; 3803cfcc809SGreg Roach 3813cfcc809SGreg Roach return $this->viewResponse('modules/relationships-chart/config', [ 3823cfcc809SGreg Roach 'all_trees' => Tree::getAll(), 3833cfcc809SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 3843cfcc809SGreg Roach 'default_ancestors' => self::DEFAULT_ANCESTORS, 3853cfcc809SGreg Roach 'default_recursion' => self::DEFAULT_RECURSION, 3863cfcc809SGreg Roach 'recursion_options' => $this->recursionConfigOptions(), 3873cfcc809SGreg Roach 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), 3883cfcc809SGreg Roach ]); 3893cfcc809SGreg Roach } 3903cfcc809SGreg Roach 3913cfcc809SGreg Roach /** 3923cfcc809SGreg Roach * @param ServerRequestInterface $request 3933cfcc809SGreg Roach * 3943cfcc809SGreg Roach * @return ResponseInterface 3953cfcc809SGreg Roach */ 3963cfcc809SGreg Roach public function postAdminAction(ServerRequestInterface $request): ResponseInterface 3973cfcc809SGreg Roach { 3983cfcc809SGreg Roach foreach (Tree::getAll() as $tree) { 3993cfcc809SGreg Roach $recursion = $request->getParsedBody()['relationship-recursion-' . $tree->id()] ?? ''; 4003cfcc809SGreg Roach $ancestors = $request->getParsedBody()['relationship-ancestors-' . $tree->id()] ?? ''; 4013cfcc809SGreg Roach 4023cfcc809SGreg Roach $tree->setPreference('RELATIONSHIP_RECURSION', $recursion); 4033cfcc809SGreg Roach $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors); 4043cfcc809SGreg Roach } 4053cfcc809SGreg Roach 4063cfcc809SGreg Roach FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success'); 4073cfcc809SGreg Roach 4083cfcc809SGreg Roach return redirect($this->getConfigLink()); 4093cfcc809SGreg Roach } 4103cfcc809SGreg Roach 4113cfcc809SGreg Roach /** 4123cfcc809SGreg Roach * Possible options for the ancestors option 4133cfcc809SGreg Roach * 4143cfcc809SGreg Roach * @return string[] 4153cfcc809SGreg Roach */ 4163cfcc809SGreg Roach private function ancestorsOptions(): array 4173cfcc809SGreg Roach { 4183cfcc809SGreg Roach return [ 4193cfcc809SGreg Roach 0 => I18N::translate('Find any relationship'), 4203cfcc809SGreg Roach 1 => I18N::translate('Find relationships via ancestors'), 4213cfcc809SGreg Roach ]; 4223cfcc809SGreg Roach } 4233cfcc809SGreg Roach 4243cfcc809SGreg Roach /** 4253cfcc809SGreg Roach * Possible options for the recursion option 4263cfcc809SGreg Roach * 4273cfcc809SGreg Roach * @return string[] 4283cfcc809SGreg Roach */ 4293cfcc809SGreg Roach private function recursionConfigOptions(): array 4303cfcc809SGreg Roach { 4313cfcc809SGreg Roach return [ 4323cfcc809SGreg Roach 0 => I18N::translate('none'), 4333cfcc809SGreg Roach 1 => I18N::number(1), 4343cfcc809SGreg Roach 2 => I18N::number(2), 4353cfcc809SGreg Roach 3 => I18N::number(3), 4363cfcc809SGreg Roach self::UNLIMITED_RECURSION => I18N::translate('unlimited'), 4373cfcc809SGreg Roach ]; 4383cfcc809SGreg Roach } 4393cfcc809SGreg Roach 4403cfcc809SGreg Roach /** 4419b5537c3SGreg Roach * Calculate the shortest paths - or all paths - between two individuals. 4429b5537c3SGreg Roach * 4439b5537c3SGreg Roach * @param Individual $individual1 4449b5537c3SGreg Roach * @param Individual $individual2 4459b5537c3SGreg Roach * @param int $recursion How many levels of recursion to use 4469b5537c3SGreg Roach * @param bool $ancestor Restrict to relationships via a common ancestor 4479b5537c3SGreg Roach * 4489b5537c3SGreg Roach * @return string[][] 4499b5537c3SGreg Roach */ 4509b5537c3SGreg Roach private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array 4519b5537c3SGreg Roach { 4523dcc812bSGreg Roach $tree = $individual1->tree(); 4533dcc812bSGreg Roach 4549b5537c3SGreg Roach $rows = DB::table('link') 4553dcc812bSGreg Roach ->where('l_file', '=', $tree->id()) 4569b5537c3SGreg Roach ->whereIn('l_type', ['FAMS', 'FAMC']) 4579b5537c3SGreg Roach ->select(['l_from', 'l_to']) 4589b5537c3SGreg Roach ->get(); 4599b5537c3SGreg Roach 4609b5537c3SGreg Roach // Optionally restrict the graph to the ancestors of the individuals. 4619b5537c3SGreg Roach if ($ancestor) { 4623dcc812bSGreg Roach $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id()); 4633dcc812bSGreg Roach $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id()); 4649b5537c3SGreg Roach } else { 4659b5537c3SGreg Roach $ancestors = []; 4669b5537c3SGreg Roach $exclude = []; 4679b5537c3SGreg Roach } 4689b5537c3SGreg Roach 4699b5537c3SGreg Roach $graph = []; 4709b5537c3SGreg Roach 4719b5537c3SGreg Roach foreach ($rows as $row) { 47222d65e5aSGreg Roach if (empty($ancestors) || in_array($row->l_from, $ancestors, true) && !in_array($row->l_to, $exclude, true)) { 4739b5537c3SGreg Roach $graph[$row->l_from][$row->l_to] = 1; 4749b5537c3SGreg Roach $graph[$row->l_to][$row->l_from] = 1; 4759b5537c3SGreg Roach } 4769b5537c3SGreg Roach } 4779b5537c3SGreg Roach 4789b5537c3SGreg Roach $xref1 = $individual1->xref(); 4799b5537c3SGreg Roach $xref2 = $individual2->xref(); 4809b5537c3SGreg Roach $dijkstra = new Dijkstra($graph); 4819b5537c3SGreg Roach $paths = $dijkstra->shortestPaths($xref1, $xref2); 4829b5537c3SGreg Roach 4839b5537c3SGreg Roach // Only process each exclusion list once; 4849b5537c3SGreg Roach $excluded = []; 4859b5537c3SGreg Roach 4869b5537c3SGreg Roach $queue = []; 4879b5537c3SGreg Roach foreach ($paths as $path) { 4889b5537c3SGreg Roach // Insert the paths into the queue, with an exclusion list. 4899b5537c3SGreg Roach $queue[] = [ 4909b5537c3SGreg Roach 'path' => $path, 4919b5537c3SGreg Roach 'exclude' => [], 4929b5537c3SGreg Roach ]; 4939b5537c3SGreg Roach // While there are un-extended paths 4949b5537c3SGreg Roach for ($next = current($queue); $next !== false; $next = next($queue)) { 4959b5537c3SGreg Roach // For each family on the path 4969b5537c3SGreg Roach for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { 4979b5537c3SGreg Roach $exclude = $next['exclude']; 4989b5537c3SGreg Roach if (count($exclude) >= $recursion) { 4999b5537c3SGreg Roach continue; 5009b5537c3SGreg Roach } 5019b5537c3SGreg Roach $exclude[] = $next['path'][$n]; 5029b5537c3SGreg Roach sort($exclude); 5039b5537c3SGreg Roach $tmp = implode('-', $exclude); 50422d65e5aSGreg Roach if (in_array($tmp, $excluded, true)) { 5059b5537c3SGreg Roach continue; 5069b5537c3SGreg Roach } 5079b5537c3SGreg Roach 5089b5537c3SGreg Roach $excluded[] = $tmp; 5099b5537c3SGreg Roach // Add any new path to the queue 5109b5537c3SGreg Roach foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { 5119b5537c3SGreg Roach $queue[] = [ 5129b5537c3SGreg Roach 'path' => $new_path, 5139b5537c3SGreg Roach 'exclude' => $exclude, 5149b5537c3SGreg Roach ]; 5159b5537c3SGreg Roach } 5169b5537c3SGreg Roach } 5179b5537c3SGreg Roach } 5189b5537c3SGreg Roach } 519185cbb4dSGreg Roach // Extract the paths from the queue. 5209b5537c3SGreg Roach $paths = []; 5219b5537c3SGreg Roach foreach ($queue as $next) { 522185cbb4dSGreg Roach // The Dijkstra library does not use strict types, and converts 523185cbb4dSGreg Roach // numeric array keys (XREFs) from strings to integers; 524185cbb4dSGreg Roach $path = array_map($this->stringMapper(), $next['path']); 525185cbb4dSGreg Roach 526185cbb4dSGreg Roach // Remove duplicates 527185cbb4dSGreg Roach $paths[implode('-', $next['path'])] = $path; 5289b5537c3SGreg Roach } 5299b5537c3SGreg Roach 5309b5537c3SGreg Roach return $paths; 5319b5537c3SGreg Roach } 5329b5537c3SGreg Roach 5339b5537c3SGreg Roach /** 534185cbb4dSGreg Roach * Convert numeric values to strings 535185cbb4dSGreg Roach * 536185cbb4dSGreg Roach * @return Closure 537185cbb4dSGreg Roach */ 538c3f3b628SGreg Roach private function stringMapper(): Closure 539c3f3b628SGreg Roach { 540185cbb4dSGreg Roach return static function ($xref) { 541185cbb4dSGreg Roach return (string) $xref; 542185cbb4dSGreg Roach }; 543185cbb4dSGreg Roach } 544185cbb4dSGreg Roach 545185cbb4dSGreg Roach /** 5469b5537c3SGreg Roach * Find all ancestors of a list of individuals 5479b5537c3SGreg Roach * 5489b5537c3SGreg Roach * @param string $xref1 5499b5537c3SGreg Roach * @param string $xref2 5509b5537c3SGreg Roach * @param int $tree_id 5519b5537c3SGreg Roach * 5529b5537c3SGreg Roach * @return string[] 5539b5537c3SGreg Roach */ 5549b5537c3SGreg Roach private function allAncestors($xref1, $xref2, $tree_id): array 5559b5537c3SGreg Roach { 5569b5537c3SGreg Roach $ancestors = [ 5579b5537c3SGreg Roach $xref1, 5589b5537c3SGreg Roach $xref2, 5599b5537c3SGreg Roach ]; 5609b5537c3SGreg Roach 5619b5537c3SGreg Roach $queue = [ 5629b5537c3SGreg Roach $xref1, 5639b5537c3SGreg Roach $xref2, 5649b5537c3SGreg Roach ]; 5659b5537c3SGreg Roach while (!empty($queue)) { 5669b5537c3SGreg Roach $parents = DB::table('link AS l1') 5670b5fd0a6SGreg Roach ->join('link AS l2', static function (JoinClause $join): void { 5689b5537c3SGreg Roach $join 5699b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 5709b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 5719b5537c3SGreg Roach }) 5729b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 5739b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMC') 5749b5537c3SGreg Roach ->where('l2.l_type', '=', 'FAMS') 5759b5537c3SGreg Roach ->whereIn('l1.l_from', $queue) 5769b5537c3SGreg Roach ->pluck('l2.l_from'); 5779b5537c3SGreg Roach 5789b5537c3SGreg Roach $queue = []; 5799b5537c3SGreg Roach foreach ($parents as $parent) { 58022d65e5aSGreg Roach if (!in_array($parent, $ancestors, true)) { 5819b5537c3SGreg Roach $ancestors[] = $parent; 5829b5537c3SGreg Roach $queue[] = $parent; 5839b5537c3SGreg Roach } 5849b5537c3SGreg Roach } 5859b5537c3SGreg Roach } 5869b5537c3SGreg Roach 5879b5537c3SGreg Roach return $ancestors; 5889b5537c3SGreg Roach } 5899b5537c3SGreg Roach 5909b5537c3SGreg Roach /** 5919b5537c3SGreg Roach * Find all families of two individuals 5929b5537c3SGreg Roach * 5939b5537c3SGreg Roach * @param string $xref1 5949b5537c3SGreg Roach * @param string $xref2 5959b5537c3SGreg Roach * @param int $tree_id 5969b5537c3SGreg Roach * 5979b5537c3SGreg Roach * @return string[] 5989b5537c3SGreg Roach */ 5999b5537c3SGreg Roach private function excludeFamilies($xref1, $xref2, $tree_id): array 6009b5537c3SGreg Roach { 6019b5537c3SGreg Roach return DB::table('link AS l1') 6020b5fd0a6SGreg Roach ->join('link AS l2', static function (JoinClause $join): void { 6039b5537c3SGreg Roach $join 6049b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 6059b5537c3SGreg Roach ->on('l1.l_type', '=', 'l2.l_type') 6069b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 6079b5537c3SGreg Roach }) 6089b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 6099b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMS') 6109b5537c3SGreg Roach ->where('l1.l_from', '=', $xref1) 6119b5537c3SGreg Roach ->where('l2.l_from', '=', $xref2) 6129b5537c3SGreg Roach ->pluck('l1.l_to') 6139b5537c3SGreg Roach ->all(); 6149b5537c3SGreg Roach } 6159b5537c3SGreg Roach 6169b5537c3SGreg Roach /** 6176ccdf4f0SGreg Roach * Convert a path (list of XREFs) to an "old-style" string of relationships. 6186ccdf4f0SGreg Roach * Return an empty array, if privacy rules prevent us viewing any node. 6196ccdf4f0SGreg Roach * 6206ccdf4f0SGreg Roach * @param Tree $tree 6216ccdf4f0SGreg Roach * @param string[] $path Alternately Individual / Family 6226ccdf4f0SGreg Roach * 6236ccdf4f0SGreg Roach * @return string[] 6246ccdf4f0SGreg Roach */ 6256ccdf4f0SGreg Roach private function oldStyleRelationshipPath(Tree $tree, array $path): array 6266ccdf4f0SGreg Roach { 6276ccdf4f0SGreg Roach $spouse_codes = [ 6286ccdf4f0SGreg Roach 'M' => 'hus', 6296ccdf4f0SGreg Roach 'F' => 'wif', 6306ccdf4f0SGreg Roach 'U' => 'spo', 6316ccdf4f0SGreg Roach ]; 6326ccdf4f0SGreg Roach $parent_codes = [ 6336ccdf4f0SGreg Roach 'M' => 'fat', 6346ccdf4f0SGreg Roach 'F' => 'mot', 6356ccdf4f0SGreg Roach 'U' => 'par', 6366ccdf4f0SGreg Roach ]; 6376ccdf4f0SGreg Roach $child_codes = [ 6386ccdf4f0SGreg Roach 'M' => 'son', 6396ccdf4f0SGreg Roach 'F' => 'dau', 6406ccdf4f0SGreg Roach 'U' => 'chi', 6416ccdf4f0SGreg Roach ]; 6426ccdf4f0SGreg Roach $sibling_codes = [ 6436ccdf4f0SGreg Roach 'M' => 'bro', 6446ccdf4f0SGreg Roach 'F' => 'sis', 6456ccdf4f0SGreg Roach 'U' => 'sib', 6466ccdf4f0SGreg Roach ]; 6476ccdf4f0SGreg Roach $relationships = []; 6486ccdf4f0SGreg Roach 6496ccdf4f0SGreg Roach for ($i = 1, $count = count($path); $i < $count; $i += 2) { 6506ccdf4f0SGreg Roach $family = Family::getInstance($path[$i], $tree); 6516ccdf4f0SGreg Roach $prev = Individual::getInstance($path[$i - 1], $tree); 6526ccdf4f0SGreg Roach $next = Individual::getInstance($path[$i + 1], $tree); 6536ccdf4f0SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { 6546ccdf4f0SGreg Roach $rel1 = $match[1]; 6556ccdf4f0SGreg Roach } else { 6566ccdf4f0SGreg Roach return []; 6576ccdf4f0SGreg Roach } 6586ccdf4f0SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { 6596ccdf4f0SGreg Roach $rel2 = $match[1]; 6606ccdf4f0SGreg Roach } else { 6616ccdf4f0SGreg Roach return []; 6626ccdf4f0SGreg Roach } 6636ccdf4f0SGreg Roach if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 6646ccdf4f0SGreg Roach $relationships[$i] = $spouse_codes[$next->sex()]; 6656ccdf4f0SGreg Roach } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { 6666ccdf4f0SGreg Roach $relationships[$i] = $child_codes[$next->sex()]; 6676ccdf4f0SGreg Roach } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 6686ccdf4f0SGreg Roach $relationships[$i] = $parent_codes[$next->sex()]; 6696ccdf4f0SGreg Roach } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { 6706ccdf4f0SGreg Roach $relationships[$i] = $sibling_codes[$next->sex()]; 6716ccdf4f0SGreg Roach } 6726ccdf4f0SGreg Roach } 6736ccdf4f0SGreg Roach 6746ccdf4f0SGreg Roach return $relationships; 6756ccdf4f0SGreg Roach } 6766ccdf4f0SGreg Roach 6776ccdf4f0SGreg Roach /** 6789b5537c3SGreg Roach * Possible options for the recursion option 6799b5537c3SGreg Roach * 6809b5537c3SGreg Roach * @param int $max_recursion 6819b5537c3SGreg Roach * 6824db4b4a9SGreg Roach * @return string[] 6839b5537c3SGreg Roach */ 6849b5537c3SGreg Roach private function recursionOptions(int $max_recursion): array 6859b5537c3SGreg Roach { 6863dcc812bSGreg Roach if ($max_recursion === static::UNLIMITED_RECURSION) { 6879b5537c3SGreg Roach $text = I18N::translate('Find all possible relationships'); 6889b5537c3SGreg Roach } else { 6899b5537c3SGreg Roach $text = I18N::translate('Find other relationships'); 6909b5537c3SGreg Roach } 6919b5537c3SGreg Roach 6929b5537c3SGreg Roach return [ 6939b5537c3SGreg Roach '0' => I18N::translate('Find the closest relationships'), 6949b5537c3SGreg Roach $max_recursion => $text, 6959b5537c3SGreg Roach ]; 6969b5537c3SGreg Roach } 697168ff6f3Sric2016} 698