1168ff6f3Sric2016<?php 2168ff6f3Sric2016/** 3168ff6f3Sric2016 * webtrees: online genealogy 48fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team 5168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify 6168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by 7168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or 8168ff6f3Sric2016 * (at your option) any later version. 9168ff6f3Sric2016 * This program is distributed in the hope that it will be useful, 10168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12168ff6f3Sric2016 * GNU General Public License for more details. 13168ff6f3Sric2016 * You should have received a copy of the GNU General Public License 14168ff6f3Sric2016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15168ff6f3Sric2016 */ 16e7f56f2aSGreg Roachdeclare(strict_types=1); 17e7f56f2aSGreg Roach 18168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 19168ff6f3Sric2016 209b5537c3SGreg Roachuse Fisharebest\Algorithm\Dijkstra; 21168ff6f3Sric2016use Fisharebest\Webtrees\Auth; 229b5537c3SGreg Roachuse Fisharebest\Webtrees\Family; 2345ac604bSGreg Roachuse Fisharebest\Webtrees\FlashMessages; 249b5537c3SGreg Roachuse Fisharebest\Webtrees\FontAwesome; 259b5537c3SGreg Roachuse Fisharebest\Webtrees\Functions\Functions; 269b5537c3SGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsPrint; 27168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 28168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 291e3273c9SGreg Roachuse Fisharebest\Webtrees\Menu; 309b5537c3SGreg Roachuse Fisharebest\Webtrees\Theme; 3145ac604bSGreg Roachuse Fisharebest\Webtrees\Tree; 329867b2f0SGreg Roachuse Fisharebest\Webtrees\User; 339b5537c3SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 349b5537c3SGreg Roachuse Illuminate\Database\Query\JoinClause; 35291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\RedirectResponse; 36291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\Request; 37291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\Response; 38168ff6f3Sric2016 39168ff6f3Sric2016/** 40168ff6f3Sric2016 * Class RelationshipsChartModule 41168ff6f3Sric2016 */ 4237eb8894SGreg Roachclass RelationshipsChartModule extends AbstractModule implements ModuleChartInterface, ModuleConfigInterface 43c1010edaSGreg Roach{ 4449a243cbSGreg Roach use ModuleChartTrait; 4549a243cbSGreg Roach use ModuleConfigTrait; 4649a243cbSGreg Roach 471e3273c9SGreg Roach /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */ 4816d6367aSGreg Roach public const UNLIMITED_RECURSION = 99; 491e3273c9SGreg Roach 501e3273c9SGreg Roach /** By default new trees allow unlimited recursion */ 5116d6367aSGreg Roach public const DEFAULT_RECURSION = '99'; 5245ac604bSGreg Roach 53e0bd7dc9SGreg Roach /** By default new trees search for all relationships (not via ancestors) */ 5416d6367aSGreg Roach public const DEFAULT_ANCESTORS = '0'; 55e0bd7dc9SGreg Roach 56168ff6f3Sric2016 /** 57168ff6f3Sric2016 * How should this module be labelled on tabs, menus, etc.? 58168ff6f3Sric2016 * 59168ff6f3Sric2016 * @return string 60168ff6f3Sric2016 */ 6149a243cbSGreg Roach public function title(): string 62c1010edaSGreg Roach { 63bbb76c12SGreg Roach /* I18N: Name of a module/chart */ 64bbb76c12SGreg Roach return I18N::translate('Relationships'); 65168ff6f3Sric2016 } 66168ff6f3Sric2016 67168ff6f3Sric2016 /** 68168ff6f3Sric2016 * A sentence describing what this module does. 69168ff6f3Sric2016 * 70168ff6f3Sric2016 * @return string 71168ff6f3Sric2016 */ 7249a243cbSGreg Roach public function description(): string 73c1010edaSGreg Roach { 74bbb76c12SGreg Roach /* I18N: Description of the “RelationshipsChart” module */ 75bbb76c12SGreg Roach return I18N::translate('A chart displaying relationships between two individuals.'); 76168ff6f3Sric2016 } 77168ff6f3Sric2016 78168ff6f3Sric2016 /** 79e6562982SGreg Roach * A main menu item for this chart. 80168ff6f3Sric2016 * 818e69695bSGreg Roach * @param Individual $individual 828e69695bSGreg Roach * 83e6562982SGreg Roach * @return Menu 84168ff6f3Sric2016 */ 85e6562982SGreg Roach public function chartMenu(Individual $individual): Menu 86c1010edaSGreg Roach { 87e6562982SGreg Roach $gedcomid = $individual->tree()->getUserPreference(Auth::user(), 'gedcomid'); 88168ff6f3Sric2016 893dcc812bSGreg Roach if ($gedcomid !== '' && $gedcomid !== $individual->xref()) { 90168ff6f3Sric2016 return new Menu( 91168ff6f3Sric2016 I18N::translate('Relationship to me'), 92e6562982SGreg Roach $this->chartUrl($individual, ['xref2' => $gedcomid]), 93377a2979SGreg Roach $this->chartMenuClass(), 94e6562982SGreg Roach $this->chartUrlAttributes() 95168ff6f3Sric2016 ); 96b2ce94c6SRico Sonntag } 97b2ce94c6SRico Sonntag 98168ff6f3Sric2016 return new Menu( 99e6562982SGreg Roach $this->title(), 100e6562982SGreg Roach $this->chartUrl($individual), 101377a2979SGreg Roach $this->chartMenuClass(), 102e6562982SGreg Roach $this->chartUrlAttributes() 103168ff6f3Sric2016 ); 104168ff6f3Sric2016 } 105168ff6f3Sric2016 1064eb71cfaSGreg Roach /** 107377a2979SGreg Roach * CSS class for the URL. 108377a2979SGreg Roach * 109377a2979SGreg Roach * @return string 110377a2979SGreg Roach */ 111377a2979SGreg Roach public function chartMenuClass(): string 112377a2979SGreg Roach { 113377a2979SGreg Roach return 'menu-chart-relationship'; 114377a2979SGreg Roach } 115377a2979SGreg Roach 116377a2979SGreg Roach /** 1174eb71cfaSGreg Roach * Return a menu item for this chart - for use in individual boxes. 1184eb71cfaSGreg Roach * 1198e69695bSGreg Roach * @param Individual $individual 1208e69695bSGreg Roach * 1214eb71cfaSGreg Roach * @return Menu|null 1224eb71cfaSGreg Roach */ 123377a2979SGreg Roach public function chartBoxMenu(Individual $individual): ?Menu 124c1010edaSGreg Roach { 125e6562982SGreg Roach return $this->chartMenu($individual); 126e6562982SGreg Roach } 127e6562982SGreg Roach 128e6562982SGreg Roach /** 129291c1b19SGreg Roach * @return Response 130291c1b19SGreg Roach */ 1310120d29dSGreg Roach public function getAdminAction(): Response 132c1010edaSGreg Roach { 133291c1b19SGreg Roach $this->layout = 'layouts/administration'; 134291c1b19SGreg Roach 1359b5537c3SGreg Roach return $this->viewResponse('modules/relationships-chart/config', [ 136291c1b19SGreg Roach 'all_trees' => Tree::getAll(), 137291c1b19SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 138291c1b19SGreg Roach 'default_ancestors' => self::DEFAULT_ANCESTORS, 139291c1b19SGreg Roach 'default_recursion' => self::DEFAULT_RECURSION, 1409b5537c3SGreg Roach 'recursion_options' => $this->recursionConfigOptions(), 14149a243cbSGreg Roach 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), 142291c1b19SGreg Roach ]); 143291c1b19SGreg Roach } 144291c1b19SGreg Roach 145291c1b19SGreg Roach /** 146291c1b19SGreg Roach * @param Request $request 147291c1b19SGreg Roach * 148291c1b19SGreg Roach * @return RedirectResponse 149291c1b19SGreg Roach */ 150c1010edaSGreg Roach public function postAdminAction(Request $request): RedirectResponse 151c1010edaSGreg Roach { 152291c1b19SGreg Roach foreach (Tree::getAll() as $tree) { 15372cf66d4SGreg Roach $recursion = $request->get('relationship-recursion-' . $tree->id(), ''); 15472cf66d4SGreg Roach $ancestors = $request->get('relationship-ancestors-' . $tree->id(), ''); 15575ee5198SGreg Roach 15675ee5198SGreg Roach $tree->setPreference('RELATIONSHIP_RECURSION', $recursion); 15775ee5198SGreg Roach $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors); 158291c1b19SGreg Roach } 159291c1b19SGreg Roach 16049a243cbSGreg Roach FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success'); 161291c1b19SGreg Roach 162291c1b19SGreg Roach return new RedirectResponse($this->getConfigLink()); 163291c1b19SGreg Roach } 164291c1b19SGreg Roach 16545ac604bSGreg Roach /** 166e0bd7dc9SGreg Roach * Possible options for the ancestors option 16718d7a90dSGreg Roach * 16818d7a90dSGreg Roach * @return string[] 169e0bd7dc9SGreg Roach */ 17018d7a90dSGreg Roach private function ancestorsOptions(): array 171c1010edaSGreg Roach { 17213abd6f3SGreg Roach return [ 173e0bd7dc9SGreg Roach 0 => I18N::translate('Find any relationship'), 174e0bd7dc9SGreg Roach 1 => I18N::translate('Find relationships via ancestors'), 17513abd6f3SGreg Roach ]; 176e0bd7dc9SGreg Roach } 177e0bd7dc9SGreg Roach 178e0bd7dc9SGreg Roach /** 1791e3273c9SGreg Roach * Possible options for the recursion option 18018d7a90dSGreg Roach * 18118d7a90dSGreg Roach * @return string[] 1821e3273c9SGreg Roach */ 1839b5537c3SGreg Roach private function recursionConfigOptions(): array 184c1010edaSGreg Roach { 18513abd6f3SGreg Roach return [ 1861e3273c9SGreg Roach 0 => I18N::translate('none'), 1871e3273c9SGreg Roach 1 => I18N::number(1), 1881e3273c9SGreg Roach 2 => I18N::number(2), 1891e3273c9SGreg Roach 3 => I18N::number(3), 190e0bd7dc9SGreg Roach self::UNLIMITED_RECURSION => I18N::translate('unlimited'), 19113abd6f3SGreg Roach ]; 1921e3273c9SGreg Roach } 1939b5537c3SGreg Roach 1949b5537c3SGreg Roach /** 1959b5537c3SGreg Roach * A form to request the chart parameters. 1969b5537c3SGreg Roach * 1979b5537c3SGreg Roach * @param Request $request 1989b5537c3SGreg Roach * @param Tree $tree 1999867b2f0SGreg Roach * @param User $user 2009b5537c3SGreg Roach * 2019b5537c3SGreg Roach * @return Response 2029b5537c3SGreg Roach */ 2039867b2f0SGreg Roach public function getChartAction(Request $request, Tree $tree, User $user): Response 2049b5537c3SGreg Roach { 2059b5537c3SGreg Roach $ajax = (bool) $request->get('ajax'); 2069b5537c3SGreg Roach 2073dcc812bSGreg Roach $xref = $request->get('xref', ''); 2089b5537c3SGreg Roach $xref2 = $request->get('xref2', ''); 2099b5537c3SGreg Roach 2103dcc812bSGreg Roach $individual1 = Individual::getInstance($xref, $tree); 2119b5537c3SGreg Roach $individual2 = Individual::getInstance($xref2, $tree); 2129b5537c3SGreg Roach 2139b5537c3SGreg Roach $recursion = (int) $request->get('recursion', '0'); 2149b5537c3SGreg Roach $ancestors = (int) $request->get('ancestors', '0'); 2159b5537c3SGreg Roach 2163dcc812bSGreg Roach $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS); 2173dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2189b5537c3SGreg Roach 2199b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 2209b5537c3SGreg Roach 2213dcc812bSGreg Roach if ($individual1 instanceof Individual) { 2223dcc812bSGreg Roach Auth::checkIndividualAccess($individual1); 2233dcc812bSGreg Roach } 2243dcc812bSGreg Roach 2253dcc812bSGreg Roach if ($individual2 instanceof Individual) { 2263dcc812bSGreg Roach Auth::checkIndividualAccess($individual2); 2273dcc812bSGreg Roach } 2283dcc812bSGreg Roach 2299867b2f0SGreg Roach Auth::checkComponentAccess($this, 'chart', $tree, $user); 2309867b2f0SGreg Roach 2319b5537c3SGreg Roach if ($individual1 instanceof Individual && $individual2 instanceof Individual) { 232f866a2aeSGreg Roach if ($ajax) { 233f866a2aeSGreg Roach return $this->chart($individual1, $individual2, $recursion, $ancestors); 234f866a2aeSGreg Roach } 235f866a2aeSGreg Roach 2369b5537c3SGreg Roach /* I18N: %s are individual’s names */ 2379b5537c3SGreg Roach $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->getFullName(), $individual2->getFullName()); 238f866a2aeSGreg Roach 2399b5537c3SGreg Roach $ajax_url = $this->chartUrl($individual1, [ 2409b5537c3SGreg Roach 'ajax' => true, 241f866a2aeSGreg Roach 'xref2' => $individual2->xref(), 2429b5537c3SGreg Roach 'recursion' => $recursion, 2439b5537c3SGreg Roach 'ancestors' => $ancestors, 2449b5537c3SGreg Roach ]); 2453dcc812bSGreg Roach } else { 2463dcc812bSGreg Roach $title = I18N::translate('Relationships'); 2473dcc812bSGreg Roach 248f866a2aeSGreg Roach $ajax_url = ''; 2493dcc812bSGreg Roach } 2509b5537c3SGreg Roach 2519b5537c3SGreg Roach return $this->viewResponse('modules/relationships-chart/page', [ 2529b5537c3SGreg Roach 'ajax_url' => $ajax_url, 2539b5537c3SGreg Roach 'ancestors' => $ancestors, 2549b5537c3SGreg Roach 'ancestors_only' => $ancestors_only, 2559b5537c3SGreg Roach 'ancestors_options' => $this->ancestorsOptions(), 2569b5537c3SGreg Roach 'individual1' => $individual1, 2579b5537c3SGreg Roach 'individual2' => $individual2, 2589b5537c3SGreg Roach 'max_recursion' => $max_recursion, 2599b5537c3SGreg Roach 'module_name' => $this->name(), 2609b5537c3SGreg Roach 'recursion' => $recursion, 2619b5537c3SGreg Roach 'recursion_options' => $this->recursionOptions($max_recursion), 2629b5537c3SGreg Roach 'title' => $title, 2639b5537c3SGreg Roach ]); 2649b5537c3SGreg Roach } 2659b5537c3SGreg Roach 2669b5537c3SGreg Roach /** 2673dcc812bSGreg Roach * @param Individual $individual1 2683dcc812bSGreg Roach * @param Individual $individual2 2693dcc812bSGreg Roach * @param int $recursion 2703dcc812bSGreg Roach * @param int $ancestors 2719b5537c3SGreg Roach * 2729b5537c3SGreg Roach * @return Response 2739b5537c3SGreg Roach */ 2743dcc812bSGreg Roach public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): Response 2759b5537c3SGreg Roach { 2763dcc812bSGreg Roach $tree = $individual1->tree(); 2779b5537c3SGreg Roach 2783dcc812bSGreg Roach $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION); 2799b5537c3SGreg Roach 2809b5537c3SGreg Roach $recursion = min($recursion, $max_recursion); 2819b5537c3SGreg Roach 2829b5537c3SGreg Roach $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); 2839b5537c3SGreg Roach 2849b5537c3SGreg Roach // @TODO - convert to views 2859b5537c3SGreg Roach ob_start(); 2869b5537c3SGreg Roach if (I18N::direction() === 'ltr') { 287*8136679eSGreg Roach $diagonal1 = app()->make(ModuleThemeInterface::class)->parameter('image-dline'); 288*8136679eSGreg Roach $diagonal2 = app()->make(ModuleThemeInterface::class)->parameter('image-dline2'); 2899b5537c3SGreg Roach } else { 290*8136679eSGreg Roach $diagonal1 = app()->make(ModuleThemeInterface::class)->parameter('image-dline2'); 291*8136679eSGreg Roach $diagonal2 = app()->make(ModuleThemeInterface::class)->parameter('image-dline'); 2929b5537c3SGreg Roach } 2939b5537c3SGreg Roach 2949b5537c3SGreg Roach $num_paths = 0; 2959b5537c3SGreg Roach foreach ($paths as $path) { 2969b5537c3SGreg Roach // Extract the relationship names between pairs of individuals 2979b5537c3SGreg Roach $relationships = $this->oldStyleRelationshipPath($tree, $path); 2989b5537c3SGreg Roach if (empty($relationships)) { 2999b5537c3SGreg Roach // Cannot see one of the families/individuals, due to privacy; 3009b5537c3SGreg Roach continue; 3019b5537c3SGreg Roach } 3029b5537c3SGreg Roach echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>'; 3039b5537c3SGreg Roach $num_paths++; 3049b5537c3SGreg Roach 3059b5537c3SGreg Roach // Use a table/grid for layout. 3069b5537c3SGreg Roach $table = []; 3079b5537c3SGreg Roach // Current position in the grid. 3089b5537c3SGreg Roach $x = 0; 3099b5537c3SGreg Roach $y = 0; 3109b5537c3SGreg Roach // Extent of the grid. 3119b5537c3SGreg Roach $min_y = 0; 3129b5537c3SGreg Roach $max_y = 0; 3139b5537c3SGreg Roach $max_x = 0; 3149b5537c3SGreg Roach // For each node in the path. 3159b5537c3SGreg Roach foreach ($path as $n => $xref) { 3169b5537c3SGreg Roach if ($n % 2 === 1) { 3179b5537c3SGreg Roach switch ($relationships[$n]) { 3189b5537c3SGreg Roach case 'hus': 3199b5537c3SGreg Roach case 'wif': 3209b5537c3SGreg Roach case 'spo': 3219b5537c3SGreg Roach case 'bro': 3229b5537c3SGreg Roach case 'sis': 3239b5537c3SGreg Roach case 'sib': 324*8136679eSGreg Roach $table[$x + 1][$y] = '<div style="background:url(' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . ') 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-end') . '</div></div>'; 3259b5537c3SGreg Roach $x += 2; 3269b5537c3SGreg Roach break; 3279b5537c3SGreg Roach case 'son': 3289b5537c3SGreg Roach case 'dau': 3299b5537c3SGreg Roach case 'chi': 3309b5537c3SGreg Roach if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { 331d993d560SGreg 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>'; 3329b5537c3SGreg Roach $x += 2; 3339b5537c3SGreg Roach } else { 334*8136679eSGreg Roach $table[$x][$y - 1] = '<div style="background:url(' . app()->make(ModuleThemeInterface::class) 335d993d560SGreg Roach ->parameter('image-vline') . ') 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>'; 3369b5537c3SGreg Roach } 3379b5537c3SGreg Roach $y -= 2; 3389b5537c3SGreg Roach break; 3399b5537c3SGreg Roach case 'fat': 3409b5537c3SGreg Roach case 'mot': 3419b5537c3SGreg Roach case 'par': 3429b5537c3SGreg Roach if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { 343d993d560SGreg 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>'; 3449b5537c3SGreg Roach $x += 2; 3459b5537c3SGreg Roach } else { 346*8136679eSGreg Roach $table[$x][$y + 1] = '<div style="background:url(' . app()->make(ModuleThemeInterface::class) 347d993d560SGreg Roach ->parameter('image-vline') . ') 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>'; 3489b5537c3SGreg Roach } 3499b5537c3SGreg Roach $y += 2; 3509b5537c3SGreg Roach break; 3519b5537c3SGreg Roach } 3529b5537c3SGreg Roach $max_x = max($max_x, $x); 3539b5537c3SGreg Roach $min_y = min($min_y, $y); 3549b5537c3SGreg Roach $max_y = max($max_y, $y); 3559b5537c3SGreg Roach } else { 3569b5537c3SGreg Roach $individual = Individual::getInstance($xref, $tree); 3579b5537c3SGreg Roach $table[$x][$y] = FunctionsPrint::printPedigreePerson($individual); 3589b5537c3SGreg Roach } 3599b5537c3SGreg Roach } 3609b5537c3SGreg Roach echo '<div class="wt-chart wt-relationship-chart">'; 3619b5537c3SGreg Roach echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; 3629b5537c3SGreg Roach for ($y = $max_y; $y >= $min_y; --$y) { 3639b5537c3SGreg Roach echo '<tr>'; 3649b5537c3SGreg Roach for ($x = 0; $x <= $max_x; ++$x) { 3659b5537c3SGreg Roach echo '<td style="padding: 0;">'; 3669b5537c3SGreg Roach if (isset($table[$x][$y])) { 3679b5537c3SGreg Roach echo $table[$x][$y]; 3689b5537c3SGreg Roach } 3699b5537c3SGreg Roach echo '</td>'; 3709b5537c3SGreg Roach } 3719b5537c3SGreg Roach echo '</tr>'; 3729b5537c3SGreg Roach } 3739b5537c3SGreg Roach echo '</table>'; 3749b5537c3SGreg Roach echo '</div>'; 3759b5537c3SGreg Roach } 3769b5537c3SGreg Roach 3779b5537c3SGreg Roach if (!$num_paths) { 3789b5537c3SGreg Roach echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; 3799b5537c3SGreg Roach } 3809b5537c3SGreg Roach 3819b5537c3SGreg Roach $html = ob_get_clean(); 3829b5537c3SGreg Roach 3839b5537c3SGreg Roach return new Response($html); 3849b5537c3SGreg Roach } 3859b5537c3SGreg Roach 3869b5537c3SGreg Roach /** 3879b5537c3SGreg Roach * Calculate the shortest paths - or all paths - between two individuals. 3889b5537c3SGreg Roach * 3899b5537c3SGreg Roach * @param Individual $individual1 3909b5537c3SGreg Roach * @param Individual $individual2 3919b5537c3SGreg Roach * @param int $recursion How many levels of recursion to use 3929b5537c3SGreg Roach * @param bool $ancestor Restrict to relationships via a common ancestor 3939b5537c3SGreg Roach * 3949b5537c3SGreg Roach * @return string[][] 3959b5537c3SGreg Roach */ 3969b5537c3SGreg Roach private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array 3979b5537c3SGreg Roach { 3983dcc812bSGreg Roach $tree = $individual1->tree(); 3993dcc812bSGreg Roach 4009b5537c3SGreg Roach $rows = DB::table('link') 4013dcc812bSGreg Roach ->where('l_file', '=', $tree->id()) 4029b5537c3SGreg Roach ->whereIn('l_type', ['FAMS', 'FAMC']) 4039b5537c3SGreg Roach ->select(['l_from', 'l_to']) 4049b5537c3SGreg Roach ->get(); 4059b5537c3SGreg Roach 4069b5537c3SGreg Roach // Optionally restrict the graph to the ancestors of the individuals. 4079b5537c3SGreg Roach if ($ancestor) { 4083dcc812bSGreg Roach $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id()); 4093dcc812bSGreg Roach $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id()); 4109b5537c3SGreg Roach } else { 4119b5537c3SGreg Roach $ancestors = []; 4129b5537c3SGreg Roach $exclude = []; 4139b5537c3SGreg Roach } 4149b5537c3SGreg Roach 4159b5537c3SGreg Roach $graph = []; 4169b5537c3SGreg Roach 4179b5537c3SGreg Roach foreach ($rows as $row) { 4189b5537c3SGreg Roach if (empty($ancestors) || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) { 4199b5537c3SGreg Roach $graph[$row->l_from][$row->l_to] = 1; 4209b5537c3SGreg Roach $graph[$row->l_to][$row->l_from] = 1; 4219b5537c3SGreg Roach } 4229b5537c3SGreg Roach } 4239b5537c3SGreg Roach 4249b5537c3SGreg Roach $xref1 = $individual1->xref(); 4259b5537c3SGreg Roach $xref2 = $individual2->xref(); 4269b5537c3SGreg Roach $dijkstra = new Dijkstra($graph); 4279b5537c3SGreg Roach $paths = $dijkstra->shortestPaths($xref1, $xref2); 4289b5537c3SGreg Roach 4299b5537c3SGreg Roach // Only process each exclusion list once; 4309b5537c3SGreg Roach $excluded = []; 4319b5537c3SGreg Roach 4329b5537c3SGreg Roach $queue = []; 4339b5537c3SGreg Roach foreach ($paths as $path) { 4349b5537c3SGreg Roach // Insert the paths into the queue, with an exclusion list. 4359b5537c3SGreg Roach $queue[] = [ 4369b5537c3SGreg Roach 'path' => $path, 4379b5537c3SGreg Roach 'exclude' => [], 4389b5537c3SGreg Roach ]; 4399b5537c3SGreg Roach // While there are un-extended paths 4409b5537c3SGreg Roach for ($next = current($queue); $next !== false; $next = next($queue)) { 4419b5537c3SGreg Roach // For each family on the path 4429b5537c3SGreg Roach for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { 4439b5537c3SGreg Roach $exclude = $next['exclude']; 4449b5537c3SGreg Roach if (count($exclude) >= $recursion) { 4459b5537c3SGreg Roach continue; 4469b5537c3SGreg Roach } 4479b5537c3SGreg Roach $exclude[] = $next['path'][$n]; 4489b5537c3SGreg Roach sort($exclude); 4499b5537c3SGreg Roach $tmp = implode('-', $exclude); 4509b5537c3SGreg Roach if (in_array($tmp, $excluded)) { 4519b5537c3SGreg Roach continue; 4529b5537c3SGreg Roach } 4539b5537c3SGreg Roach 4549b5537c3SGreg Roach $excluded[] = $tmp; 4559b5537c3SGreg Roach // Add any new path to the queue 4569b5537c3SGreg Roach foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { 4579b5537c3SGreg Roach $queue[] = [ 4589b5537c3SGreg Roach 'path' => $new_path, 4599b5537c3SGreg Roach 'exclude' => $exclude, 4609b5537c3SGreg Roach ]; 4619b5537c3SGreg Roach } 4629b5537c3SGreg Roach } 4639b5537c3SGreg Roach } 4649b5537c3SGreg Roach } 4659b5537c3SGreg Roach // Extract the paths from the queue, removing duplicates. 4669b5537c3SGreg Roach $paths = []; 4679b5537c3SGreg Roach foreach ($queue as $next) { 4689b5537c3SGreg Roach $paths[implode('-', $next['path'])] = $next['path']; 4699b5537c3SGreg Roach } 4709b5537c3SGreg Roach 4719b5537c3SGreg Roach return $paths; 4729b5537c3SGreg Roach } 4739b5537c3SGreg Roach 4749b5537c3SGreg Roach /** 4759b5537c3SGreg Roach * Convert a path (list of XREFs) to an "old-style" string of relationships. 4769b5537c3SGreg Roach * Return an empty array, if privacy rules prevent us viewing any node. 4779b5537c3SGreg Roach * 4789b5537c3SGreg Roach * @param Tree $tree 4799b5537c3SGreg Roach * @param string[] $path Alternately Individual / Family 4809b5537c3SGreg Roach * 4819b5537c3SGreg Roach * @return string[] 4829b5537c3SGreg Roach */ 4839b5537c3SGreg Roach private function oldStyleRelationshipPath(Tree $tree, array $path): array 4849b5537c3SGreg Roach { 4859b5537c3SGreg Roach $spouse_codes = [ 4869b5537c3SGreg Roach 'M' => 'hus', 4879b5537c3SGreg Roach 'F' => 'wif', 4889b5537c3SGreg Roach 'U' => 'spo', 4899b5537c3SGreg Roach ]; 4909b5537c3SGreg Roach $parent_codes = [ 4919b5537c3SGreg Roach 'M' => 'fat', 4929b5537c3SGreg Roach 'F' => 'mot', 4939b5537c3SGreg Roach 'U' => 'par', 4949b5537c3SGreg Roach ]; 4959b5537c3SGreg Roach $child_codes = [ 4969b5537c3SGreg Roach 'M' => 'son', 4979b5537c3SGreg Roach 'F' => 'dau', 4989b5537c3SGreg Roach 'U' => 'chi', 4999b5537c3SGreg Roach ]; 5009b5537c3SGreg Roach $sibling_codes = [ 5019b5537c3SGreg Roach 'M' => 'bro', 5029b5537c3SGreg Roach 'F' => 'sis', 5039b5537c3SGreg Roach 'U' => 'sib', 5049b5537c3SGreg Roach ]; 5059b5537c3SGreg Roach $relationships = []; 5069b5537c3SGreg Roach 5079b5537c3SGreg Roach for ($i = 1, $count = count($path); $i < $count; $i += 2) { 5089b5537c3SGreg Roach $family = Family::getInstance($path[$i], $tree); 5099b5537c3SGreg Roach $prev = Individual::getInstance($path[$i - 1], $tree); 5109b5537c3SGreg Roach $next = Individual::getInstance($path[$i + 1], $tree); 5119b5537c3SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { 5129b5537c3SGreg Roach $rel1 = $match[1]; 5139b5537c3SGreg Roach } else { 5149b5537c3SGreg Roach return []; 5159b5537c3SGreg Roach } 5169b5537c3SGreg Roach if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { 5179b5537c3SGreg Roach $rel2 = $match[1]; 5189b5537c3SGreg Roach } else { 5199b5537c3SGreg Roach return []; 5209b5537c3SGreg Roach } 5219b5537c3SGreg Roach if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 5229b5537c3SGreg Roach $relationships[$i] = $spouse_codes[$next->getSex()]; 5239b5537c3SGreg Roach } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { 5249b5537c3SGreg Roach $relationships[$i] = $child_codes[$next->getSex()]; 5259b5537c3SGreg Roach } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { 5269b5537c3SGreg Roach $relationships[$i] = $parent_codes[$next->getSex()]; 5279b5537c3SGreg Roach } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { 5289b5537c3SGreg Roach $relationships[$i] = $sibling_codes[$next->getSex()]; 5299b5537c3SGreg Roach } 5309b5537c3SGreg Roach } 5319b5537c3SGreg Roach 5329b5537c3SGreg Roach return $relationships; 5339b5537c3SGreg Roach } 5349b5537c3SGreg Roach 5359b5537c3SGreg Roach /** 5369b5537c3SGreg Roach * Find all ancestors of a list of individuals 5379b5537c3SGreg Roach * 5389b5537c3SGreg Roach * @param string $xref1 5399b5537c3SGreg Roach * @param string $xref2 5409b5537c3SGreg Roach * @param int $tree_id 5419b5537c3SGreg Roach * 5429b5537c3SGreg Roach * @return string[] 5439b5537c3SGreg Roach */ 5449b5537c3SGreg Roach private function allAncestors($xref1, $xref2, $tree_id): array 5459b5537c3SGreg Roach { 5469b5537c3SGreg Roach $ancestors = [ 5479b5537c3SGreg Roach $xref1, 5489b5537c3SGreg Roach $xref2, 5499b5537c3SGreg Roach ]; 5509b5537c3SGreg Roach 5519b5537c3SGreg Roach $queue = [ 5529b5537c3SGreg Roach $xref1, 5539b5537c3SGreg Roach $xref2, 5549b5537c3SGreg Roach ]; 5559b5537c3SGreg Roach while (!empty($queue)) { 5569b5537c3SGreg Roach $parents = DB::table('link AS l1') 5579b5537c3SGreg Roach ->join('link AS l2', function (JoinClause $join): void { 5589b5537c3SGreg Roach $join 5599b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 5609b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 5619b5537c3SGreg Roach }) 5629b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 5639b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMC') 5649b5537c3SGreg Roach ->where('l2.l_type', '=', 'FAMS') 5659b5537c3SGreg Roach ->whereIn('l1.l_from', $queue) 5669b5537c3SGreg Roach ->pluck('l2.l_from'); 5679b5537c3SGreg Roach 5689b5537c3SGreg Roach $queue = []; 5699b5537c3SGreg Roach foreach ($parents as $parent) { 5709b5537c3SGreg Roach if (!in_array($parent, $ancestors)) { 5719b5537c3SGreg Roach $ancestors[] = $parent; 5729b5537c3SGreg Roach $queue[] = $parent; 5739b5537c3SGreg Roach } 5749b5537c3SGreg Roach } 5759b5537c3SGreg Roach } 5769b5537c3SGreg Roach 5779b5537c3SGreg Roach return $ancestors; 5789b5537c3SGreg Roach } 5799b5537c3SGreg Roach 5809b5537c3SGreg Roach /** 5819b5537c3SGreg Roach * Find all families of two individuals 5829b5537c3SGreg Roach * 5839b5537c3SGreg Roach * @param string $xref1 5849b5537c3SGreg Roach * @param string $xref2 5859b5537c3SGreg Roach * @param int $tree_id 5869b5537c3SGreg Roach * 5879b5537c3SGreg Roach * @return string[] 5889b5537c3SGreg Roach */ 5899b5537c3SGreg Roach private function excludeFamilies($xref1, $xref2, $tree_id): array 5909b5537c3SGreg Roach { 5919b5537c3SGreg Roach return DB::table('link AS l1') 5929b5537c3SGreg Roach ->join('link AS l2', function (JoinClause $join): void { 5939b5537c3SGreg Roach $join 5949b5537c3SGreg Roach ->on('l1.l_to', '=', 'l2.l_to') 5959b5537c3SGreg Roach ->on('l1.l_type', '=', 'l2.l_type') 5969b5537c3SGreg Roach ->on('l1.l_file', '=', 'l2.l_file'); 5979b5537c3SGreg Roach }) 5989b5537c3SGreg Roach ->where('l1.l_file', '=', $tree_id) 5999b5537c3SGreg Roach ->where('l1.l_type', '=', 'FAMS') 6009b5537c3SGreg Roach ->where('l1.l_from', '=', $xref1) 6019b5537c3SGreg Roach ->where('l2.l_from', '=', $xref2) 6029b5537c3SGreg Roach ->pluck('l1.l_to') 6039b5537c3SGreg Roach ->all(); 6049b5537c3SGreg Roach } 6059b5537c3SGreg Roach 6069b5537c3SGreg Roach /** 6079b5537c3SGreg Roach * Possible options for the recursion option 6089b5537c3SGreg Roach * 6099b5537c3SGreg Roach * @param int $max_recursion 6109b5537c3SGreg Roach * 6119b5537c3SGreg Roach * @return array 6129b5537c3SGreg Roach */ 6139b5537c3SGreg Roach private function recursionOptions(int $max_recursion): array 6149b5537c3SGreg Roach { 6153dcc812bSGreg Roach if ($max_recursion === static::UNLIMITED_RECURSION) { 6169b5537c3SGreg Roach $text = I18N::translate('Find all possible relationships'); 6179b5537c3SGreg Roach } else { 6189b5537c3SGreg Roach $text = I18N::translate('Find other relationships'); 6199b5537c3SGreg Roach } 6209b5537c3SGreg Roach 6219b5537c3SGreg Roach return [ 6229b5537c3SGreg Roach '0' => I18N::translate('Find the closest relationships'), 6239b5537c3SGreg Roach $max_recursion => $text, 6249b5537c3SGreg Roach ]; 6259b5537c3SGreg Roach } 626168ff6f3Sric2016} 627