diff options
| -rw-r--r-- | app/Http/Controllers/RelationshipsChartController.php | 473 | ||||
| -rw-r--r-- | app/Module/AncestorsChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/ChartsBlockModule.php | 6 | ||||
| -rw-r--r-- | app/Module/CompactTreeChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/DescendancyChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/FamilyBookChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/FanChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/HourglassChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/InteractiveTree/TreeView.php | 2 | ||||
| -rw-r--r-- | app/Module/LifespansChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/PedigreeChartModule.php | 8 | ||||
| -rw-r--r-- | app/Module/RelationshipsChartModule.php | 455 | ||||
| -rw-r--r-- | app/Module/StatisticsChartModule.php | 2 | ||||
| -rw-r--r-- | app/Module/TimelineChartModule.php | 8 | ||||
| -rw-r--r-- | resources/views/modules/ancestors-chart/page.phtml (renamed from resources/views/modules/ancestors-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/compact-chart/page.phtml (renamed from resources/views/modules/compact-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/descendancy_chart/page.phtml (renamed from resources/views/modules/descendancy_chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/family-book-chart/page.phtml (renamed from resources/views/modules/family-book-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/fanchart/page.phtml (renamed from resources/views/modules/fanchart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/hourglass-chart/page.phtml (renamed from resources/views/modules/hourglass-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/lifespans-chart/page.phtml (renamed from resources/views/modules/lifespans-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/pedigree-chart/page.phtml (renamed from resources/views/modules/pedigree-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/relationships-chart/config.phtml (renamed from resources/views/modules/relationships_chart/config.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/relationships-chart/page.phtml (renamed from resources/views/relationships-page.phtml) | 12 | ||||
| -rw-r--r-- | resources/views/modules/statistics-chart/page.phtml (renamed from resources/views/modules/statistics-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | resources/views/modules/timeline-chart/page.phtml (renamed from resources/views/modules/timeline-chart/chart-page.phtml) | 0 | ||||
| -rw-r--r-- | routes/web.php | 2 |
27 files changed, 493 insertions, 531 deletions
diff --git a/app/Http/Controllers/RelationshipsChartController.php b/app/Http/Controllers/RelationshipsChartController.php deleted file mode 100644 index 0c3d45db06..0000000000 --- a/app/Http/Controllers/RelationshipsChartController.php +++ /dev/null @@ -1,473 +0,0 @@ -<?php -/** - * webtrees: online genealogy - * Copyright (C) 2019 webtrees development team - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -declare(strict_types=1); - -namespace Fisharebest\Webtrees\Http\Controllers; - -use Fisharebest\Algorithm\Dijkstra; -use Fisharebest\Webtrees\Auth; -use Fisharebest\Webtrees\Family; -use Fisharebest\Webtrees\FontAwesome; -use Fisharebest\Webtrees\Functions\Functions; -use Fisharebest\Webtrees\Functions\FunctionsPrint; -use Fisharebest\Webtrees\I18N; -use Fisharebest\Webtrees\Individual; -use Fisharebest\Webtrees\Module\RelationshipsChartModule; -use Fisharebest\Webtrees\Theme; -use Fisharebest\Webtrees\Tree; -use Illuminate\Database\Capsule\Manager as DB; -use Illuminate\Database\Query\JoinClause; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * A chart showing the relationship(s) between two individuals. - */ -class RelationshipsChartController extends AbstractBaseController -{ - /** - * A form to request the chart parameters. - * - * @param Request $request - * @param Tree $tree - * - * @return Response - */ - public function page(Request $request, Tree $tree): Response - { - $this->checkModuleIsActive($tree, RelationshipsChartModule::class); - - $xref1 = $request->get('xref1', ''); - $xref2 = $request->get('xref2', ''); - - $individual1 = Individual::getInstance($xref1, $tree); - $individual2 = Individual::getInstance($xref2, $tree); - - $recursion = (int) $request->get('recursion', '0'); - $ancestors = (int) $request->get('ancestors', '0'); - - if ($individual1 instanceof Individual && $individual2 instanceof Individual) { - Auth::checkIndividualAccess($individual1); - Auth::checkIndividualAccess($individual2); - } - - $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', RelationshipsChartModule::DEFAULT_ANCESTORS); - $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', RelationshipsChartModule::DEFAULT_RECURSION); - - $recursion = min($recursion, $max_recursion); - - if ($individual1 instanceof Individual && $individual2 instanceof Individual) { - /* I18N: %s are individual’s names */ - $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->getFullName(), $individual2->getFullName()); - } else { - $title = I18N::translate('Relationships'); - } - - return $this->viewResponse('relationships-page', [ - 'ancestors' => $ancestors, - 'ancestors_only' => $ancestors_only, - 'ancestors_options' => $this->ancestorsOptions(), - 'individual1' => $individual1, - 'individual2' => $individual2, - 'max_recursion' => $max_recursion, - 'recursion' => $recursion, - 'recursion_options' => $this->recursionOptions($max_recursion), - 'title' => $title, - ]); - } - - /** - * @param Request $request - * @param Tree $tree - * - * @return Response - */ - public function chart(Request $request, Tree $tree): Response - { - $this->checkModuleIsActive($tree, RelationshipsChartModule::class); - - $xref1 = $request->get('xref1', ''); - $individual1 = Individual::getInstance($xref1, $tree); - $xref2 = $request->get('xref2', ''); - $individual2 = Individual::getInstance($xref2, $tree); - - Auth::checkIndividualAccess($individual1); - Auth::checkIndividualAccess($individual2); - - $recursion = (int) $request->get('recursion', '0'); - $ancestors = (bool) $request->get('ancestors', '0'); - - $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', RelationshipsChartModule::DEFAULT_RECURSION); - - $recursion = min($recursion, $max_recursion); - - $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); - - // @TODO - convert to views - ob_start(); - if (I18N::direction() === 'ltr') { - $diagonal1 = Theme::theme()->parameter('image-dline'); - $diagonal2 = Theme::theme()->parameter('image-dline2'); - } else { - $diagonal1 = Theme::theme()->parameter('image-dline2'); - $diagonal2 = Theme::theme()->parameter('image-dline'); - } - - $num_paths = 0; - foreach ($paths as $path) { - // Extract the relationship names between pairs of individuals - $relationships = $this->oldStyleRelationshipPath($tree, $path); - if (empty($relationships)) { - // Cannot see one of the families/individuals, due to privacy; - continue; - } - echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>'; - $num_paths++; - - // Use a table/grid for layout. - $table = []; - // Current position in the grid. - $x = 0; - $y = 0; - // Extent of the grid. - $min_y = 0; - $max_y = 0; - $max_x = 0; - // For each node in the path. - foreach ($path as $n => $xref) { - if ($n % 2 === 1) { - switch ($relationships[$n]) { - case 'hus': - case 'wif': - case 'spo': - case 'bro': - case 'sis': - case 'sib': - $table[$x + 1][$y] = '<div style="background:url(' . Theme::theme()->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;">' . FontAwesome::decorativeIcon('arrow-end') . '</div></div>'; - $x += 2; - break; - case 'son': - case 'dau': - case 'chi': - if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { - $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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; - $x += 2; - } else { - $table[$x][$y - 1] = '<div style="background:url(' . Theme::theme() - ->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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; - } - $y -= 2; - break; - case 'fat': - case 'mot': - case 'par': - if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { - $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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; - $x += 2; - } else { - $table[$x][$y + 1] = '<div style="background:url(' . Theme::theme() - ->parameter('image-vline') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 32px;">' . 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">' . FontAwesome::decorativeIcon('arrow-up') . '</div></div>'; - } - $y += 2; - break; - } - $max_x = max($max_x, $x); - $min_y = min($min_y, $y); - $max_y = max($max_y, $y); - } else { - $individual = Individual::getInstance($xref, $tree); - $table[$x][$y] = FunctionsPrint::printPedigreePerson($individual); - } - } - echo '<div class="wt-chart wt-relationship-chart">'; - echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; - for ($y = $max_y; $y >= $min_y; --$y) { - echo '<tr>'; - for ($x = 0; $x <= $max_x; ++$x) { - echo '<td style="padding: 0;">'; - if (isset($table[$x][$y])) { - echo $table[$x][$y]; - } - echo '</td>'; - } - echo '</tr>'; - } - echo '</table>'; - echo '</div>'; - } - - if (!$num_paths) { - echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; - } - - $html = ob_get_clean(); - - return new Response($html); - } - - /** - * Calculate the shortest paths - or all paths - between two individuals. - * - * @param Individual $individual1 - * @param Individual $individual2 - * @param int $recursion How many levels of recursion to use - * @param bool $ancestor Restrict to relationships via a common ancestor - * - * @return string[][] - */ - private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array - { - $rows = DB::table('link') - ->where('l_file', '=', $individual1->tree()->id()) - ->whereIn('l_type', ['FAMS', 'FAMC']) - ->select(['l_from', 'l_to']) - ->get(); - - // Optionally restrict the graph to the ancestors of the individuals. - if ($ancestor) { - $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $individual1->tree()->id()); - $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $individual1->tree()->id()); - } else { - $ancestors = []; - $exclude = []; - } - - $graph = []; - - foreach ($rows as $row) { - if (empty($ancestors) || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) { - $graph[$row->l_from][$row->l_to] = 1; - $graph[$row->l_to][$row->l_from] = 1; - } - } - - $xref1 = $individual1->xref(); - $xref2 = $individual2->xref(); - $dijkstra = new Dijkstra($graph); - $paths = $dijkstra->shortestPaths($xref1, $xref2); - - // Only process each exclusion list once; - $excluded = []; - - $queue = []; - foreach ($paths as $path) { - // Insert the paths into the queue, with an exclusion list. - $queue[] = [ - 'path' => $path, - 'exclude' => [], - ]; - // While there are un-extended paths - for ($next = current($queue); $next !== false; $next = next($queue)) { - // For each family on the path - for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { - $exclude = $next['exclude']; - if (count($exclude) >= $recursion) { - continue; - } - $exclude[] = $next['path'][$n]; - sort($exclude); - $tmp = implode('-', $exclude); - if (in_array($tmp, $excluded)) { - continue; - } - - $excluded[] = $tmp; - // Add any new path to the queue - foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { - $queue[] = [ - 'path' => $new_path, - 'exclude' => $exclude, - ]; - } - } - } - } - // Extract the paths from the queue, removing duplicates. - $paths = []; - foreach ($queue as $next) { - $paths[implode('-', $next['path'])] = $next['path']; - } - - return $paths; - } - - /** - * Convert a path (list of XREFs) to an "old-style" string of relationships. - * - * Return an empty array, if privacy rules prevent us viewing any node. - * - * @param Tree $tree - * @param string[] $path Alternately Individual / Family - * - * @return string[] - */ - private function oldStyleRelationshipPath(Tree $tree, array $path): array - { - $spouse_codes = [ - 'M' => 'hus', - 'F' => 'wif', - 'U' => 'spo', - ]; - $parent_codes = [ - 'M' => 'fat', - 'F' => 'mot', - 'U' => 'par', - ]; - $child_codes = [ - 'M' => 'son', - 'F' => 'dau', - 'U' => 'chi', - ]; - $sibling_codes = [ - 'M' => 'bro', - 'F' => 'sis', - 'U' => 'sib', - ]; - $relationships = []; - - for ($i = 1, $count = count($path); $i < $count; $i += 2) { - $family = Family::getInstance($path[$i], $tree); - $prev = Individual::getInstance($path[$i - 1], $tree); - $next = Individual::getInstance($path[$i + 1], $tree); - if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { - $rel1 = $match[1]; - } else { - return []; - } - if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { - $rel2 = $match[1]; - } else { - return []; - } - if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { - $relationships[$i] = $spouse_codes[$next->getSex()]; - } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { - $relationships[$i] = $child_codes[$next->getSex()]; - } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { - $relationships[$i] = $parent_codes[$next->getSex()]; - } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { - $relationships[$i] = $sibling_codes[$next->getSex()]; - } - } - - return $relationships; - } - - /** - * Find all ancestors of a list of individuals - * - * @param string $xref1 - * @param string $xref2 - * @param int $tree_id - * - * @return string[] - */ - private function allAncestors($xref1, $xref2, $tree_id): array - { - $ancestors = [ - $xref1, - $xref2, - ]; - - $queue = [ - $xref1, - $xref2, - ]; - while (!empty($queue)) { - $parents = DB::table('link AS l1') - ->join('link AS l2', function (JoinClause $join): void { - $join - ->on('l1.l_to', '=', 'l2.l_to') - ->on('l1.l_file', '=', 'l2.l_file'); - }) - ->where('l1.l_file', '=', $tree_id) - ->where('l1.l_type', '=', 'FAMC') - ->where('l2.l_type', '=', 'FAMS') - ->whereIn('l1.l_from', $queue) - ->pluck('l2.l_from'); - - $queue = []; - foreach ($parents as $parent) { - if (!in_array($parent, $ancestors)) { - $ancestors[] = $parent; - $queue[] = $parent; - } - } - } - - return $ancestors; - } - - /** - * Find all families of two individuals - * - * @param string $xref1 - * @param string $xref2 - * @param int $tree_id - * - * @return string[] - */ - private function excludeFamilies($xref1, $xref2, $tree_id): array - { - return DB::table('link AS l1') - ->join('link AS l2', function (JoinClause $join): void { - $join - ->on('l1.l_to', '=', 'l2.l_to') - ->on('l1.l_type', '=', 'l2.l_type') - ->on('l1.l_file', '=', 'l2.l_file'); - }) - ->where('l1.l_file', '=', $tree_id) - ->where('l1.l_type', '=', 'FAMS') - ->where('l1.l_from', '=', $xref1) - ->where('l2.l_from', '=', $xref2) - ->pluck('l1.l_to') - ->all(); - } - - /** - * Possible options for the ancestors option - * - * @return string[] - */ - private function ancestorsOptions(): array - { - return [ - 0 => I18N::translate('Find any relationship'), - 1 => I18N::translate('Find relationships via ancestors'), - ]; - } - - /** - * Possible options for the recursion option - * - * @param int $max_recursion - * - * @return array - */ - private function recursionOptions(int $max_recursion): array - { - if ($max_recursion === RelationshipsChartModule::UNLIMITED_RECURSION) { - $text = I18N::translate('Find all possible relationships'); - } else { - $text = I18N::translate('Find other relationships'); - } - - return [ - '0' => I18N::translate('Find the closest relationships'), - $max_recursion => $text, - ]; - } -} diff --git a/app/Module/AncestorsChartModule.php b/app/Module/AncestorsChartModule.php index 81d5f6249f..451027226e 100644 --- a/app/Module/AncestorsChartModule.php +++ b/app/Module/AncestorsChartModule.php @@ -119,7 +119,7 @@ class AncestorsChartModule extends AbstractModule implements ModuleInterface, Mo */ public function getChartAction(Request $request, Tree $tree, ChartService $chart_service): Response { - $ajax = $request->get('ajax'); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -136,7 +136,7 @@ class AncestorsChartModule extends AbstractModule implements ModuleInterface, Mo $generations = min($generations, $maximum_generations); $generations = max($generations, $minimum_generations); - if ($ajax === '1') { + if ($ajax) { $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); switch ($chart_style) { @@ -159,10 +159,10 @@ class AncestorsChartModule extends AbstractModule implements ModuleInterface, Mo 'generations' => $generations, 'chart_style' => $chart_style, 'show_cousins' => $show_cousins, - 'ajax' => '1', + 'ajax' => true, ]); - return $this->viewResponse('modules/ancestors-chart/chart-page', [ + return $this->viewResponse('modules/ancestors-chart/page', [ 'ajax_url' => $ajax_url, 'chart_style' => $chart_style, 'chart_styles' => $this->chartStyles(), diff --git a/app/Module/ChartsBlockModule.php b/app/Module/ChartsBlockModule.php index 11591b1b0f..d1a0d145d5 100644 --- a/app/Module/ChartsBlockModule.php +++ b/app/Module/ChartsBlockModule.php @@ -91,7 +91,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleInterface, Modul $module = Module::getModuleByClassName(PedigreeChartModule::class); $title = $module->chartTitle($person); $chart_url = $module->chartUrl($person, [ - 'ajax' => '1', + 'ajax' => true, 'generations' => 3, 'layout' => PedigreeChartModule::PORTRAIT, ]); @@ -106,7 +106,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleInterface, Modul $module = Module::getModuleByClassName(DescendancyChartModule::class); $title = $module->chartTitle($person); $chart_url = $module->chartUrl($person, [ - 'ajax' => '1', + 'ajax' => true, 'generations' => 2, 'chart_style' => DescendancyChartModule::CHART_STYLE_LIST, ]); @@ -121,7 +121,7 @@ class ChartsBlockModule extends AbstractModule implements ModuleInterface, Modul $module = Module::getModuleByClassName(HourglassChartModule::class); $title = $module->chartTitle($person); $chart_url = $module->chartUrl($person, [ - 'ajax' => '1', + 'ajax' => true, 'generations' => 2, ]); $content = view('modules/charts/chart', [ diff --git a/app/Module/CompactTreeChartModule.php b/app/Module/CompactTreeChartModule.php index 9c0e416e8b..8bd7138d61 100644 --- a/app/Module/CompactTreeChartModule.php +++ b/app/Module/CompactTreeChartModule.php @@ -101,21 +101,21 @@ class CompactTreeChartModule extends AbstractModule implements ModuleInterface, */ public function getChartAction(Request $request, Tree $tree, ChartService $chart_service): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); Auth::checkIndividualAccess($individual); - if ($ajax === '1') { + if ($ajax) { return $this->chartCompact($individual, $chart_service); } $ajax_url = $this->chartUrl($individual, [ - 'ajax' => '1', + 'ajax' => true, ]); - return $this->viewResponse('modules/compact-chart/chart-page', [ + return $this->viewResponse('modules/compact-chart/page', [ 'ajax_url' => $ajax_url, 'individual' => $individual, 'module_name' => $this->name(), diff --git a/app/Module/DescendancyChartModule.php b/app/Module/DescendancyChartModule.php index 9fdaf31986..e0dd6ff876 100644 --- a/app/Module/DescendancyChartModule.php +++ b/app/Module/DescendancyChartModule.php @@ -127,7 +127,7 @@ class DescendancyChartModule extends AbstractModule implements ModuleInterface, */ public function getChartAction(Request $request, Tree $tree, ChartService $chart_service): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -143,17 +143,17 @@ class DescendancyChartModule extends AbstractModule implements ModuleInterface, $generations = min($generations, $maximum_generations); $generations = max($generations, $minimum_generations); - if ($ajax === '1') { + if ($ajax) { return $this->chart($request, $tree, $chart_service); } $ajax_url = $this->chartUrl($individual, [ 'chart_style' => $chart_style, 'generations' => $generations, - 'ajax' => '1', + 'ajax' => true, ]); - return $this->viewResponse('modules/descendancy_chart/chart-page', [ + return $this->viewResponse('modules/descendancy_chart/page', [ 'ajax_url' => $ajax_url, 'chart_style' => $chart_style, 'chart_styles' => $this->chartStyles(), diff --git a/app/Module/FamilyBookChartModule.php b/app/Module/FamilyBookChartModule.php index 3e2214f8c4..073adf323e 100644 --- a/app/Module/FamilyBookChartModule.php +++ b/app/Module/FamilyBookChartModule.php @@ -125,7 +125,7 @@ class FamilyBookChartModule extends AbstractModule implements ModuleInterface, M */ public function getChartAction(Request $request, Tree $tree): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -145,18 +145,18 @@ class FamilyBookChartModule extends AbstractModule implements ModuleInterface, M $book_size = min($book_size, 5); $book_size = max($book_size, 2); - if ($ajax === '1') { + if ($ajax) { return $this->chart($individual, $generations, $book_size, $show_spouse); } $ajax_url = $this->chartUrl($individual, [ - 'ajax' => '1', + 'ajax' => true, 'book_size' => $book_size, 'generations' => $generations, 'show_spouse' => $show_spouse ]); - return $this->viewResponse('modules/family-book-chart/chart-page', [ + return $this->viewResponse('modules/family-book-chart/page', [ 'ajax_url' => $ajax_url, 'book_size' => $book_size, 'generations' => $generations, diff --git a/app/Module/FanChartModule.php b/app/Module/FanChartModule.php index 9cadefe8f7..a5fc062cee 100644 --- a/app/Module/FanChartModule.php +++ b/app/Module/FanChartModule.php @@ -118,7 +118,7 @@ class FanChartModule extends AbstractModule implements ModuleInterface, ModuleCh */ public function getChartAction(Request $request, Tree $tree, ChartService $chart_service): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -134,18 +134,18 @@ class FanChartModule extends AbstractModule implements ModuleInterface, ModuleCh $generations = min($generations, self::MAXIMUM_GENERATIONS); $generations = max($generations, self::MINIMUM_GENERATIONS); - if ($ajax === '1') { + if ($ajax) { return $this->chart($individual, $chart_style, $fan_width, $generations, $chart_service); } $ajax_url = $this->chartUrl($individual, [ - 'ajax' => '1', + 'ajax' => true, 'chart_style' => $chart_style, 'fan_width' => $fan_width, 'generations' => $generations, ]); - return $this->viewResponse('modules/fanchart/chart-page', [ + return $this->viewResponse('modules/fanchart/page', [ 'ajax_url' => $ajax_url, 'chart_style' => $chart_style, 'chart_styles' => $this->chartStyles(), diff --git a/app/Module/HourglassChartModule.php b/app/Module/HourglassChartModule.php index e2cd92bc0b..7f2ccb4f76 100644 --- a/app/Module/HourglassChartModule.php +++ b/app/Module/HourglassChartModule.php @@ -96,7 +96,7 @@ class HourglassChartModule extends AbstractModule implements ModuleInterface, Mo */ public function getChartAction(Request $request, Tree $tree): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -112,15 +112,15 @@ class HourglassChartModule extends AbstractModule implements ModuleInterface, Mo $show_spouse = (bool) $request->get('show_spouse'); - if ($ajax === '1') { + if ($ajax) { return $this->chart($individual, $generations, $show_spouse); } $ajax_url = $this->chartUrl($individual, [ - 'ajax' => '1', + 'ajax' => true, ]); - return $this->viewResponse('modules/hourglass-chart/chart-page', [ + return $this->viewResponse('modules/hourglass-chart/page', [ 'ajax_url' => $ajax_url, 'generations' => $generations, 'individual' => $individual, diff --git a/app/Module/InteractiveTree/TreeView.php b/app/Module/InteractiveTree/TreeView.php index b7edd5f09b..822fe8bd26 100644 --- a/app/Module/InteractiveTree/TreeView.php +++ b/app/Module/InteractiveTree/TreeView.php @@ -173,7 +173,7 @@ class TreeView * * @param Family[] $familyList array of families to draw the children for * @param int $gen number of generations to draw - * @param bool $ajax setted to true for an ajax call + * @param bool $ajax true for an ajax call * * @return string */ diff --git a/app/Module/LifespansChartModule.php b/app/Module/LifespansChartModule.php index b9900067b9..114777e3c1 100644 --- a/app/Module/LifespansChartModule.php +++ b/app/Module/LifespansChartModule.php @@ -103,7 +103,7 @@ class LifespansChartModule extends AbstractModule implements ModuleInterface, Mo */ public function getChartAction(Request $request, Tree $tree): Response { - $ajax = $request->get('ajax'); + $ajax = (bool) $request->get('ajax'); $xrefs = (array) $request->get('xrefs', []); $addxref = $request->get('addxref', ''); $addfam = (bool) $request->get('addfam', false); @@ -145,14 +145,14 @@ class LifespansChartModule extends AbstractModule implements ModuleInterface, Mo return $individual !== null && $individual->canShow(); }); - if ($ajax === '1') { + if ($ajax) { $subtitle = $this->subtitle(count($xrefs), $start_date, $end_date, $placename); return $this->chart($tree, $xrefs, $subtitle); } $ajax_url = route('module', [ - 'ajax' => '1', + 'ajax' => true, 'module' => $this->name(), 'action' => 'Chart', 'ged' => $tree->name(), @@ -165,7 +165,7 @@ class LifespansChartModule extends AbstractModule implements ModuleInterface, Mo 'ged' => $tree->name(), ]); - return $this->viewResponse('modules/lifespans-chart/chart-page', [ + return $this->viewResponse('modules/lifespans-chart/page', [ 'ajax_url' => $ajax_url, 'module_name' => $this->name(), 'reset_url' => $reset_url, diff --git a/app/Module/PedigreeChartModule.php b/app/Module/PedigreeChartModule.php index 804ed0aaae..6a60b77516 100644 --- a/app/Module/PedigreeChartModule.php +++ b/app/Module/PedigreeChartModule.php @@ -141,7 +141,7 @@ class PedigreeChartModule extends AbstractModule implements ModuleInterface, Mod */ public function getChartAction(Request $request, Tree $tree, ChartService $chart_service): Response { - $ajax = $request->get('ajax', ''); + $ajax = (bool) $request->get('ajax'); $xref = $request->get('xref', ''); $individual = Individual::getInstance($xref, $tree); @@ -155,17 +155,17 @@ class PedigreeChartModule extends AbstractModule implements ModuleInterface, Mod $generation_options = $this->generationOptions(); - if ($ajax === '1') { + if ($ajax) { return $this->chart($individual, $generations, $orientation, $chart_service); } $ajax_url = $this->chartUrl($individual, [ - 'ajax' => '1', + 'ajax' => true, 'generations' => $generations, 'orientation' => $orientation, ]); - return $this->viewResponse('modules/pedigree-chart/chart-page', [ + return $this->viewResponse('modules/pedigree-chart/page', [ 'ajax_url' => $ajax_url, 'generations' => $generations, 'generation_options' => $generation_options, diff --git a/app/Module/RelationshipsChartModule.php b/app/Module/RelationshipsChartModule.php index 656b6796d0..2aeb631b03 100644 --- a/app/Module/RelationshipsChartModule.php +++ b/app/Module/RelationshipsChartModule.php @@ -17,12 +17,20 @@ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; +use Fisharebest\Algorithm\Dijkstra; use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\FlashMessages; +use Fisharebest\Webtrees\FontAwesome; +use Fisharebest\Webtrees\Functions\Functions; +use Fisharebest\Webtrees\Functions\FunctionsPrint; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Menu; +use Fisharebest\Webtrees\Theme; use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\JoinClause; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -126,10 +134,12 @@ class RelationshipsChartModule extends AbstractModule implements ModuleInterface */ public function chartUrl(Individual $individual, array $parameters = []): string { - return route('relationships', [ - 'xref1' => $individual->xref(), - 'ged' => $individual->tree()->name(), - ] + $parameters); + return route('module', [ + 'module' => $this->name(), + 'action' => 'Chart', + 'xref1' => $individual->xref(), + 'ged' => $individual->tree()->name(), + ] + $parameters); } /** @@ -139,12 +149,12 @@ class RelationshipsChartModule extends AbstractModule implements ModuleInterface { $this->layout = 'layouts/administration'; - return $this->viewResponse('modules/relationships_chart/config', [ + return $this->viewResponse('modules/relationships-chart/config', [ 'all_trees' => Tree::getAll(), 'ancestors_options' => $this->ancestorsOptions(), 'default_ancestors' => self::DEFAULT_ANCESTORS, 'default_recursion' => self::DEFAULT_RECURSION, - 'recursion_options' => $this->recursionOptions(), + 'recursion_options' => $this->recursionConfigOptions(), 'title' => I18N::translate('Chart preferences') . ' — ' . $this->title(), ]); } @@ -187,7 +197,7 @@ class RelationshipsChartModule extends AbstractModule implements ModuleInterface * * @return string[] */ - private function recursionOptions(): array + private function recursionConfigOptions(): array { return [ 0 => I18N::translate('none'), @@ -197,4 +207,435 @@ class RelationshipsChartModule extends AbstractModule implements ModuleInterface self::UNLIMITED_RECURSION => I18N::translate('unlimited'), ]; } + + /** + * A form to request the chart parameters. + * + * @param Request $request + * @param Tree $tree + * + * @return Response + */ + public function getChartAction(Request $request, Tree $tree): Response + { + $ajax = (bool) $request->get('ajax'); + + $xref1 = $request->get('xref1', ''); + $xref2 = $request->get('xref2', ''); + + $individual1 = Individual::getInstance($xref1, $tree); + $individual2 = Individual::getInstance($xref2, $tree); + + $recursion = (int) $request->get('recursion', '0'); + $ancestors = (int) $request->get('ancestors', '0'); + + if ($individual1 instanceof Individual && $individual2 instanceof Individual) { + Auth::checkIndividualAccess($individual1); + Auth::checkIndividualAccess($individual2); + } + + $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', RelationshipsChartModule::DEFAULT_ANCESTORS); + $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', RelationshipsChartModule::DEFAULT_RECURSION); + + $recursion = min($recursion, $max_recursion); + + if ($individual1 instanceof Individual && $individual2 instanceof Individual) { + /* I18N: %s are individual’s names */ + $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->getFullName(), $individual2->getFullName()); + } else { + $title = I18N::translate('Relationships'); + } + + if ($ajax) { + return $this->chart($request, $tree); + } + + $ajax_url = $this->chartUrl($individual1, [ + 'ajax' => true, + 'xref2' => $individual2->xref(), + 'ged' => $individual2->tree()->name(), + 'recursion' => $recursion, + 'ancestors' => $ancestors, + ]); + + return $this->viewResponse('modules/relationships-chart/page', [ + 'ajax_url' => $ajax_url, + 'ancestors' => $ancestors, + 'ancestors_only' => $ancestors_only, + 'ancestors_options' => $this->ancestorsOptions(), + 'individual1' => $individual1, + 'individual2' => $individual2, + 'max_recursion' => $max_recursion, + 'module_name' => $this->name(), + 'recursion' => $recursion, + 'recursion_options' => $this->recursionOptions($max_recursion), + 'title' => $title, + ]); + } + + /** + * @param Request $request + * @param Tree $tree + * + * @return Response + */ + public function chart(Request $request, Tree $tree): Response + { + $xref1 = $request->get('xref1', ''); + $individual1 = Individual::getInstance($xref1, $tree); + $xref2 = $request->get('xref2', ''); + $individual2 = Individual::getInstance($xref2, $tree); + + Auth::checkIndividualAccess($individual1); + Auth::checkIndividualAccess($individual2); + + $recursion = (int) $request->get('recursion', '0'); + $ancestors = (bool) $request->get('ancestors', '0'); + + $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', RelationshipsChartModule::DEFAULT_RECURSION); + + $recursion = min($recursion, $max_recursion); + + $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors); + + // @TODO - convert to views + ob_start(); + if (I18N::direction() === 'ltr') { + $diagonal1 = Theme::theme()->parameter('image-dline'); + $diagonal2 = Theme::theme()->parameter('image-dline2'); + } else { + $diagonal1 = Theme::theme()->parameter('image-dline2'); + $diagonal2 = Theme::theme()->parameter('image-dline'); + } + + $num_paths = 0; + foreach ($paths as $path) { + // Extract the relationship names between pairs of individuals + $relationships = $this->oldStyleRelationshipPath($tree, $path); + if (empty($relationships)) { + // Cannot see one of the families/individuals, due to privacy; + continue; + } + echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>'; + $num_paths++; + + // Use a table/grid for layout. + $table = []; + // Current position in the grid. + $x = 0; + $y = 0; + // Extent of the grid. + $min_y = 0; + $max_y = 0; + $max_x = 0; + // For each node in the path. + foreach ($path as $n => $xref) { + if ($n % 2 === 1) { + switch ($relationships[$n]) { + case 'hus': + case 'wif': + case 'spo': + case 'bro': + case 'sis': + case 'sib': + $table[$x + 1][$y] = '<div style="background:url(' . Theme::theme()->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;">' . FontAwesome::decorativeIcon('arrow-end') . '</div></div>'; + $x += 2; + break; + case 'son': + case 'dau': + case 'chi': + if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) { + $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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; + $x += 2; + } else { + $table[$x][$y - 1] = '<div style="background:url(' . Theme::theme() + ->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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; + } + $y -= 2; + break; + case 'fat': + case 'mot': + case 'par': + if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) { + $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;">' . FontAwesome::decorativeIcon('arrow-down') . '</div></div>'; + $x += 2; + } else { + $table[$x][$y + 1] = '<div style="background:url(' . Theme::theme() + ->parameter('image-vline') . ') repeat-y center; height: 64px; text-align:center; "><div class="vline-text" style="display: inline-block; width: 50%; line-height: 32px;">' . 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">' . FontAwesome::decorativeIcon('arrow-up') . '</div></div>'; + } + $y += 2; + break; + } + $max_x = max($max_x, $x); + $min_y = min($min_y, $y); + $max_y = max($max_y, $y); + } else { + $individual = Individual::getInstance($xref, $tree); + $table[$x][$y] = FunctionsPrint::printPedigreePerson($individual); + } + } + echo '<div class="wt-chart wt-relationship-chart">'; + echo '<table style="border-collapse: collapse; margin: 20px 50px;">'; + for ($y = $max_y; $y >= $min_y; --$y) { + echo '<tr>'; + for ($x = 0; $x <= $max_x; ++$x) { + echo '<td style="padding: 0;">'; + if (isset($table[$x][$y])) { + echo $table[$x][$y]; + } + echo '</td>'; + } + echo '</tr>'; + } + echo '</table>'; + echo '</div>'; + } + + if (!$num_paths) { + echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>'; + } + + $html = ob_get_clean(); + + return new Response($html); + } + + /** + * Calculate the shortest paths - or all paths - between two individuals. + * + * @param Individual $individual1 + * @param Individual $individual2 + * @param int $recursion How many levels of recursion to use + * @param bool $ancestor Restrict to relationships via a common ancestor + * + * @return string[][] + */ + private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array + { + $rows = DB::table('link') + ->where('l_file', '=', $individual1->tree()->id()) + ->whereIn('l_type', ['FAMS', 'FAMC']) + ->select(['l_from', 'l_to']) + ->get(); + + // Optionally restrict the graph to the ancestors of the individuals. + if ($ancestor) { + $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $individual1->tree()->id()); + $exclude = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $individual1->tree()->id()); + } else { + $ancestors = []; + $exclude = []; + } + + $graph = []; + + foreach ($rows as $row) { + if (empty($ancestors) || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) { + $graph[$row->l_from][$row->l_to] = 1; + $graph[$row->l_to][$row->l_from] = 1; + } + } + + $xref1 = $individual1->xref(); + $xref2 = $individual2->xref(); + $dijkstra = new Dijkstra($graph); + $paths = $dijkstra->shortestPaths($xref1, $xref2); + + // Only process each exclusion list once; + $excluded = []; + + $queue = []; + foreach ($paths as $path) { + // Insert the paths into the queue, with an exclusion list. + $queue[] = [ + 'path' => $path, + 'exclude' => [], + ]; + // While there are un-extended paths + for ($next = current($queue); $next !== false; $next = next($queue)) { + // For each family on the path + for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) { + $exclude = $next['exclude']; + if (count($exclude) >= $recursion) { + continue; + } + $exclude[] = $next['path'][$n]; + sort($exclude); + $tmp = implode('-', $exclude); + if (in_array($tmp, $excluded)) { + continue; + } + + $excluded[] = $tmp; + // Add any new path to the queue + foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) { + $queue[] = [ + 'path' => $new_path, + 'exclude' => $exclude, + ]; + } + } + } + } + // Extract the paths from the queue, removing duplicates. + $paths = []; + foreach ($queue as $next) { + $paths[implode('-', $next['path'])] = $next['path']; + } + + return $paths; + } + + /** + * Convert a path (list of XREFs) to an "old-style" string of relationships. + * Return an empty array, if privacy rules prevent us viewing any node. + * + * @param Tree $tree + * @param string[] $path Alternately Individual / Family + * + * @return string[] + */ + private function oldStyleRelationshipPath(Tree $tree, array $path): array + { + $spouse_codes = [ + 'M' => 'hus', + 'F' => 'wif', + 'U' => 'spo', + ]; + $parent_codes = [ + 'M' => 'fat', + 'F' => 'mot', + 'U' => 'par', + ]; + $child_codes = [ + 'M' => 'son', + 'F' => 'dau', + 'U' => 'chi', + ]; + $sibling_codes = [ + 'M' => 'bro', + 'F' => 'sis', + 'U' => 'sib', + ]; + $relationships = []; + + for ($i = 1, $count = count($path); $i < $count; $i += 2) { + $family = Family::getInstance($path[$i], $tree); + $prev = Individual::getInstance($path[$i - 1], $tree); + $next = Individual::getInstance($path[$i + 1], $tree); + if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) { + $rel1 = $match[1]; + } else { + return []; + } + if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) { + $rel2 = $match[1]; + } else { + return []; + } + if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { + $relationships[$i] = $spouse_codes[$next->getSex()]; + } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') { + $relationships[$i] = $child_codes[$next->getSex()]; + } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) { + $relationships[$i] = $parent_codes[$next->getSex()]; + } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') { + $relationships[$i] = $sibling_codes[$next->getSex()]; + } + } + + return $relationships; + } + + /** + * Find all ancestors of a list of individuals + * + * @param string $xref1 + * @param string $xref2 + * @param int $tree_id + * + * @return string[] + */ + private function allAncestors($xref1, $xref2, $tree_id): array + { + $ancestors = [ + $xref1, + $xref2, + ]; + + $queue = [ + $xref1, + $xref2, + ]; + while (!empty($queue)) { + $parents = DB::table('link AS l1') + ->join('link AS l2', function (JoinClause $join): void { + $join + ->on('l1.l_to', '=', 'l2.l_to') + ->on('l1.l_file', '=', 'l2.l_file'); + }) + ->where('l1.l_file', '=', $tree_id) + ->where('l1.l_type', '=', 'FAMC') + ->where('l2.l_type', '=', 'FAMS') + ->whereIn('l1.l_from', $queue) + ->pluck('l2.l_from'); + + $queue = []; + foreach ($parents as $parent) { + if (!in_array($parent, $ancestors)) { + $ancestors[] = $parent; + $queue[] = $parent; + } + } + } + + return $ancestors; + } + + /** + * Find all families of two individuals + * + * @param string $xref1 + * @param string $xref2 + * @param int $tree_id + * + * @return string[] + */ + private function excludeFamilies($xref1, $xref2, $tree_id): array + { + return DB::table('link AS l1') + ->join('link AS l2', function (JoinClause $join): void { + $join + ->on('l1.l_to', '=', 'l2.l_to') + ->on('l1.l_type', '=', 'l2.l_type') + ->on('l1.l_file', '=', 'l2.l_file'); + }) + ->where('l1.l_file', '=', $tree_id) + ->where('l1.l_type', '=', 'FAMS') + ->where('l1.l_from', '=', $xref1) + ->where('l2.l_from', '=', $xref2) + ->pluck('l1.l_to') + ->all(); + } + + /** + * Possible options for the recursion option + * + * @param int $max_recursion + * + * @return array + */ + private function recursionOptions(int $max_recursion): array + { + if ($max_recursion === RelationshipsChartModule::UNLIMITED_RECURSION) { + $text = I18N::translate('Find all possible relationships'); + } else { + $text = I18N::translate('Find other relationships'); + } + + return [ + '0' => I18N::translate('Find the closest relationships'), + $max_recursion => $text, + ]; + } } diff --git a/app/Module/StatisticsChartModule.php b/app/Module/StatisticsChartModule.php index 813761f17d..c3a1dbfca0 100644 --- a/app/Module/StatisticsChartModule.php +++ b/app/Module/StatisticsChartModule.php @@ -148,7 +148,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleInterface, M ]), ]; - return $this->viewResponse('modules/statistics-chart/chart-page', [ + return $this->viewResponse('modules/statistics-chart/page', [ 'tabs' => $tabs, 'title' => $this->title(), ]); diff --git a/app/Module/TimelineChartModule.php b/app/Module/TimelineChartModule.php index 4f8174507b..84c9477156 100644 --- a/app/Module/TimelineChartModule.php +++ b/app/Module/TimelineChartModule.php @@ -112,7 +112,7 @@ class TimelineChartModule extends AbstractModule implements ModuleInterface, Mod */ public function getChartAction(Request $request, Tree $tree): Response { - $ajax = $request->get('ajax'); + $ajax = (bool) $request->get('ajax'); $scale = (int) $request->get('scale', self::SCALE_DEFAULT); $scale = min($scale, self::SCALE_MAX); $scale = max($scale, self::SCALE_MIN); @@ -153,12 +153,12 @@ class TimelineChartModule extends AbstractModule implements ModuleInterface, Mod return Individual::getInstance($xref, $tree); }, $xrefs); - if ($ajax === '1') { + if ($ajax) { return $this->chart($tree, $xrefs, $scale); } $ajax_url = route('module', [ - 'ajax' => '1', + 'ajax' => true, 'module' => $this->name(), 'action' => 'Chart', 'ged' => $tree->name(), @@ -188,7 +188,7 @@ class TimelineChartModule extends AbstractModule implements ModuleInterface, Mod 'xrefs' => $xrefs, ]); - return $this->viewResponse('modules/timeline-chart/chart-page', [ + return $this->viewResponse('modules/timeline-chart/page', [ 'ajax_url' => $ajax_url, 'individuals' => $individuals, 'module_name' => $this->name(), diff --git a/resources/views/modules/ancestors-chart/chart-page.phtml b/resources/views/modules/ancestors-chart/page.phtml index 5c008bae25..5c008bae25 100644 --- a/resources/views/modules/ancestors-chart/chart-page.phtml +++ b/resources/views/modules/ancestors-chart/page.phtml diff --git a/resources/views/modules/compact-chart/chart-page.phtml b/resources/views/modules/compact-chart/page.phtml index aae3243eb8..aae3243eb8 100644 --- a/resources/views/modules/compact-chart/chart-page.phtml +++ b/resources/views/modules/compact-chart/page.phtml diff --git a/resources/views/modules/descendancy_chart/chart-page.phtml b/resources/views/modules/descendancy_chart/page.phtml index 6866de25f1..6866de25f1 100644 --- a/resources/views/modules/descendancy_chart/chart-page.phtml +++ b/resources/views/modules/descendancy_chart/page.phtml diff --git a/resources/views/modules/family-book-chart/chart-page.phtml b/resources/views/modules/family-book-chart/page.phtml index 59fbfe84d4..59fbfe84d4 100644 --- a/resources/views/modules/family-book-chart/chart-page.phtml +++ b/resources/views/modules/family-book-chart/page.phtml diff --git a/resources/views/modules/fanchart/chart-page.phtml b/resources/views/modules/fanchart/page.phtml index 16688e1319..16688e1319 100644 --- a/resources/views/modules/fanchart/chart-page.phtml +++ b/resources/views/modules/fanchart/page.phtml diff --git a/resources/views/modules/hourglass-chart/chart-page.phtml b/resources/views/modules/hourglass-chart/page.phtml index 041d43acbc..041d43acbc 100644 --- a/resources/views/modules/hourglass-chart/chart-page.phtml +++ b/resources/views/modules/hourglass-chart/page.phtml diff --git a/resources/views/modules/lifespans-chart/chart-page.phtml b/resources/views/modules/lifespans-chart/page.phtml index 7b65947fcb..7b65947fcb 100644 --- a/resources/views/modules/lifespans-chart/chart-page.phtml +++ b/resources/views/modules/lifespans-chart/page.phtml diff --git a/resources/views/modules/pedigree-chart/chart-page.phtml b/resources/views/modules/pedigree-chart/page.phtml index f71b3f1c68..f71b3f1c68 100644 --- a/resources/views/modules/pedigree-chart/chart-page.phtml +++ b/resources/views/modules/pedigree-chart/page.phtml diff --git a/resources/views/modules/relationships_chart/config.phtml b/resources/views/modules/relationships-chart/config.phtml index d4212c3d3a..d4212c3d3a 100644 --- a/resources/views/modules/relationships_chart/config.phtml +++ b/resources/views/modules/relationships-chart/config.phtml diff --git a/resources/views/relationships-page.phtml b/resources/views/modules/relationships-chart/page.phtml index f15fe01698..211d995321 100644 --- a/resources/views/relationships-page.phtml +++ b/resources/views/modules/relationships-chart/page.phtml @@ -8,7 +8,9 @@ </h2> <form class="wt-page-options wt-page-options-relationships-chart d-print-none"> - <input type="hidden" name="route" value="relationships"> + <input type="hidden" name="route" value="module"> + <input type="hidden" name="module" value="<?= e($module_name) ?>"> + <input type="hidden" name="action" value="Chart"> <input type="hidden" name="ged" value="<?= e($tree->name()) ?>"> <div class="row form-group"> @@ -79,13 +81,7 @@ </form> <?php if ($individual1 !== null && $individual2 !== null) : ?> - <div class="wt-ajax-load wt-page-content wt-chart wt-relationships-chart" data-ajax-url="<?= e(route('relationships-chart', [ - 'xref1' => $individual1->xref(), - 'xref2' => $individual2->xref(), - 'ged' => $individual2->tree()->name(), - 'recursion' => $recursion, - 'ancestors' => $ancestors, - ])) ?>"></div> + <div class="wt-ajax-load wt-page-content wt-chart wt-relationships-chart" data-ajax-url="<?= e($ajax_url) ?>"></div> <?php endif ?> <?php View::push('javascript') ?> diff --git a/resources/views/modules/statistics-chart/chart-page.phtml b/resources/views/modules/statistics-chart/page.phtml index a78f389ab2..a78f389ab2 100644 --- a/resources/views/modules/statistics-chart/chart-page.phtml +++ b/resources/views/modules/statistics-chart/page.phtml diff --git a/resources/views/modules/timeline-chart/chart-page.phtml b/resources/views/modules/timeline-chart/page.phtml index a8d5441ba3..a8d5441ba3 100644 --- a/resources/views/modules/timeline-chart/chart-page.phtml +++ b/resources/views/modules/timeline-chart/page.phtml diff --git a/routes/web.php b/routes/web.php index fdc9213fe9..bc8b9c0a17 100644 --- a/routes/web.php +++ b/routes/web.php @@ -276,8 +276,6 @@ if ($tree instanceof Tree && $tree->getPreference('imported') === '1') { 'GET:source-list' => 'ListController@sourceList', 'GET:interactive' => 'InteractiveChartController@page', 'GET:interactive-chart' => 'InteractiveChartController@chart', - 'GET:relationships' => 'RelationshipsChartController@page', - 'GET:relationships-chart' => 'RelationshipsChartController@chart', 'POST:accept-changes' => 'PendingChangesController@acceptChanges', 'POST:reject-changes' => 'PendingChangesController@rejectChanges', 'POST:accept-all-changes' => 'PendingChangesController@acceptAllChanges', |
