xref: /webtrees/app/Module/RelationshipsChartModule.php (revision d993d560f991544b8dc49e013a8027c6fc967956)
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;
329b5537c3SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
339b5537c3SGreg Roachuse Illuminate\Database\Query\JoinClause;
34291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\RedirectResponse;
35291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\Request;
36291c1b19SGreg Roachuse Symfony\Component\HttpFoundation\Response;
37168ff6f3Sric2016
38168ff6f3Sric2016/**
39168ff6f3Sric2016 * Class RelationshipsChartModule
40168ff6f3Sric2016 */
4149a243cbSGreg Roachclass RelationshipsChartModule extends AbstractModule implements ModuleInterface, ModuleChartInterface, ModuleConfigInterface
42c1010edaSGreg Roach{
4349a243cbSGreg Roach    use ModuleChartTrait;
4449a243cbSGreg Roach    use ModuleConfigTrait;
4549a243cbSGreg Roach
461e3273c9SGreg Roach    /** It would be more correct to use PHP_INT_MAX, but this isn't friendly in URLs */
4716d6367aSGreg Roach    public const UNLIMITED_RECURSION = 99;
481e3273c9SGreg Roach
491e3273c9SGreg Roach    /** By default new trees allow unlimited recursion */
5016d6367aSGreg Roach    public const DEFAULT_RECURSION = '99';
5145ac604bSGreg Roach
52e0bd7dc9SGreg Roach    /** By default new trees search for all relationships (not via ancestors) */
5316d6367aSGreg Roach    public const DEFAULT_ANCESTORS = '0';
54e0bd7dc9SGreg Roach
55168ff6f3Sric2016    /**
56168ff6f3Sric2016     * How should this module be labelled on tabs, menus, etc.?
57168ff6f3Sric2016     *
58168ff6f3Sric2016     * @return string
59168ff6f3Sric2016     */
6049a243cbSGreg Roach    public function title(): string
61c1010edaSGreg Roach    {
62bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
63bbb76c12SGreg Roach        return I18N::translate('Relationships');
64168ff6f3Sric2016    }
65168ff6f3Sric2016
66168ff6f3Sric2016    /**
67168ff6f3Sric2016     * A sentence describing what this module does.
68168ff6f3Sric2016     *
69168ff6f3Sric2016     * @return string
70168ff6f3Sric2016     */
7149a243cbSGreg Roach    public function description(): string
72c1010edaSGreg Roach    {
73bbb76c12SGreg Roach        /* I18N: Description of the “RelationshipsChart” module */
74bbb76c12SGreg Roach        return I18N::translate('A chart displaying relationships between two individuals.');
75168ff6f3Sric2016    }
76168ff6f3Sric2016
77168ff6f3Sric2016    /**
78e6562982SGreg Roach     * A main menu item for this chart.
79168ff6f3Sric2016     *
808e69695bSGreg Roach     * @param Individual $individual
818e69695bSGreg Roach     *
82e6562982SGreg Roach     * @return Menu
83168ff6f3Sric2016     */
84e6562982SGreg Roach    public function chartMenu(Individual $individual): Menu
85c1010edaSGreg Roach    {
86e6562982SGreg Roach        $gedcomid = $individual->tree()->getUserPreference(Auth::user(), 'gedcomid');
87168ff6f3Sric2016
883dcc812bSGreg Roach        if ($gedcomid !== '' && $gedcomid !== $individual->xref()) {
89168ff6f3Sric2016            return new Menu(
90168ff6f3Sric2016                I18N::translate('Relationship to me'),
91e6562982SGreg Roach                $this->chartUrl($individual, ['xref2' => $gedcomid]),
92377a2979SGreg Roach                $this->chartMenuClass(),
93e6562982SGreg Roach                $this->chartUrlAttributes()
94168ff6f3Sric2016            );
95b2ce94c6SRico Sonntag        }
96b2ce94c6SRico Sonntag
97168ff6f3Sric2016        return new Menu(
98e6562982SGreg Roach            $this->title(),
99e6562982SGreg Roach            $this->chartUrl($individual),
100377a2979SGreg Roach            $this->chartMenuClass(),
101e6562982SGreg Roach            $this->chartUrlAttributes()
102168ff6f3Sric2016        );
103168ff6f3Sric2016    }
104168ff6f3Sric2016
1054eb71cfaSGreg Roach    /**
106377a2979SGreg Roach     * CSS class for the URL.
107377a2979SGreg Roach     *
108377a2979SGreg Roach     * @return string
109377a2979SGreg Roach     */
110377a2979SGreg Roach    public function chartMenuClass(): string
111377a2979SGreg Roach    {
112377a2979SGreg Roach        return 'menu-chart-relationship';
113377a2979SGreg Roach    }
114377a2979SGreg Roach
115377a2979SGreg Roach    /**
1164eb71cfaSGreg Roach     * Return a menu item for this chart - for use in individual boxes.
1174eb71cfaSGreg Roach     *
1188e69695bSGreg Roach     * @param Individual $individual
1198e69695bSGreg Roach     *
1204eb71cfaSGreg Roach     * @return Menu|null
1214eb71cfaSGreg Roach     */
122377a2979SGreg Roach    public function chartBoxMenu(Individual $individual): ?Menu
123c1010edaSGreg Roach    {
124e6562982SGreg Roach        return $this->chartMenu($individual);
125e6562982SGreg Roach    }
126e6562982SGreg Roach
127e6562982SGreg Roach    /**
128291c1b19SGreg Roach     * @return Response
129291c1b19SGreg Roach     */
1300120d29dSGreg Roach    public function getAdminAction(): Response
131c1010edaSGreg Roach    {
132291c1b19SGreg Roach        $this->layout = 'layouts/administration';
133291c1b19SGreg Roach
1349b5537c3SGreg Roach        return $this->viewResponse('modules/relationships-chart/config', [
135291c1b19SGreg Roach            'all_trees'         => Tree::getAll(),
136291c1b19SGreg Roach            'ancestors_options' => $this->ancestorsOptions(),
137291c1b19SGreg Roach            'default_ancestors' => self::DEFAULT_ANCESTORS,
138291c1b19SGreg Roach            'default_recursion' => self::DEFAULT_RECURSION,
1399b5537c3SGreg Roach            'recursion_options' => $this->recursionConfigOptions(),
14049a243cbSGreg Roach            'title'             => I18N::translate('Chart preferences') . ' — ' . $this->title(),
141291c1b19SGreg Roach        ]);
142291c1b19SGreg Roach    }
143291c1b19SGreg Roach
144291c1b19SGreg Roach    /**
145291c1b19SGreg Roach     * @param Request $request
146291c1b19SGreg Roach     *
147291c1b19SGreg Roach     * @return RedirectResponse
148291c1b19SGreg Roach     */
149c1010edaSGreg Roach    public function postAdminAction(Request $request): RedirectResponse
150c1010edaSGreg Roach    {
151291c1b19SGreg Roach        foreach (Tree::getAll() as $tree) {
15272cf66d4SGreg Roach            $recursion = $request->get('relationship-recursion-' . $tree->id(), '');
15372cf66d4SGreg Roach            $ancestors = $request->get('relationship-ancestors-' . $tree->id(), '');
15475ee5198SGreg Roach
15575ee5198SGreg Roach            $tree->setPreference('RELATIONSHIP_RECURSION', $recursion);
15675ee5198SGreg Roach            $tree->setPreference('RELATIONSHIP_ANCESTORS', $ancestors);
157291c1b19SGreg Roach        }
158291c1b19SGreg Roach
15949a243cbSGreg Roach        FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been updated.', $this->title()), 'success');
160291c1b19SGreg Roach
161291c1b19SGreg Roach        return new RedirectResponse($this->getConfigLink());
162291c1b19SGreg Roach    }
163291c1b19SGreg Roach
16445ac604bSGreg Roach    /**
165e0bd7dc9SGreg Roach     * Possible options for the ancestors option
16618d7a90dSGreg Roach     *
16718d7a90dSGreg Roach     * @return string[]
168e0bd7dc9SGreg Roach     */
16918d7a90dSGreg Roach    private function ancestorsOptions(): array
170c1010edaSGreg Roach    {
17113abd6f3SGreg Roach        return [
172e0bd7dc9SGreg Roach            0 => I18N::translate('Find any relationship'),
173e0bd7dc9SGreg Roach            1 => I18N::translate('Find relationships via ancestors'),
17413abd6f3SGreg Roach        ];
175e0bd7dc9SGreg Roach    }
176e0bd7dc9SGreg Roach
177e0bd7dc9SGreg Roach    /**
1781e3273c9SGreg Roach     * Possible options for the recursion option
17918d7a90dSGreg Roach     *
18018d7a90dSGreg Roach     * @return string[]
1811e3273c9SGreg Roach     */
1829b5537c3SGreg Roach    private function recursionConfigOptions(): array
183c1010edaSGreg Roach    {
18413abd6f3SGreg Roach        return [
1851e3273c9SGreg Roach            0                         => I18N::translate('none'),
1861e3273c9SGreg Roach            1                         => I18N::number(1),
1871e3273c9SGreg Roach            2                         => I18N::number(2),
1881e3273c9SGreg Roach            3                         => I18N::number(3),
189e0bd7dc9SGreg Roach            self::UNLIMITED_RECURSION => I18N::translate('unlimited'),
19013abd6f3SGreg Roach        ];
1911e3273c9SGreg Roach    }
1929b5537c3SGreg Roach
1939b5537c3SGreg Roach    /**
1949b5537c3SGreg Roach     * A form to request the chart parameters.
1959b5537c3SGreg Roach     *
1969b5537c3SGreg Roach     * @param Request $request
1979b5537c3SGreg Roach     * @param Tree    $tree
1989b5537c3SGreg Roach     *
1999b5537c3SGreg Roach     * @return Response
2009b5537c3SGreg Roach     */
2019b5537c3SGreg Roach    public function getChartAction(Request $request, Tree $tree): Response
2029b5537c3SGreg Roach    {
2039b5537c3SGreg Roach        $ajax = (bool) $request->get('ajax');
2049b5537c3SGreg Roach
2053dcc812bSGreg Roach        $xref  = $request->get('xref', '');
2069b5537c3SGreg Roach        $xref2 = $request->get('xref2', '');
2079b5537c3SGreg Roach
2083dcc812bSGreg Roach        $individual1 = Individual::getInstance($xref, $tree);
2099b5537c3SGreg Roach        $individual2 = Individual::getInstance($xref2, $tree);
2109b5537c3SGreg Roach
2119b5537c3SGreg Roach        $recursion = (int) $request->get('recursion', '0');
2129b5537c3SGreg Roach        $ancestors = (int) $request->get('ancestors', '0');
2139b5537c3SGreg Roach
2143dcc812bSGreg Roach        $ancestors_only = (int) $tree->getPreference('RELATIONSHIP_ANCESTORS', static::DEFAULT_ANCESTORS);
2153dcc812bSGreg Roach        $max_recursion  = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
2169b5537c3SGreg Roach
2179b5537c3SGreg Roach        $recursion = min($recursion, $max_recursion);
2189b5537c3SGreg Roach
2193dcc812bSGreg Roach        if ($individual1 instanceof Individual) {
2203dcc812bSGreg Roach            Auth::checkIndividualAccess($individual1);
2213dcc812bSGreg Roach        }
2223dcc812bSGreg Roach
2233dcc812bSGreg Roach        if ($individual2 instanceof Individual) {
2243dcc812bSGreg Roach            Auth::checkIndividualAccess($individual2);
2253dcc812bSGreg Roach        }
2263dcc812bSGreg Roach
2279b5537c3SGreg Roach        if ($individual1 instanceof Individual && $individual2 instanceof Individual) {
228f866a2aeSGreg Roach            if ($ajax) {
229f866a2aeSGreg Roach                return $this->chart($individual1, $individual2, $recursion, $ancestors);
230f866a2aeSGreg Roach            }
231f866a2aeSGreg Roach
2329b5537c3SGreg Roach            /* I18N: %s are individual’s names */
2339b5537c3SGreg Roach            $title = I18N::translate('Relationships between %1$s and %2$s', $individual1->getFullName(), $individual2->getFullName());
234f866a2aeSGreg Roach
2359b5537c3SGreg Roach            $ajax_url = $this->chartUrl($individual1, [
2369b5537c3SGreg Roach                'ajax'      => true,
237f866a2aeSGreg Roach                'xref2'     => $individual2->xref(),
2389b5537c3SGreg Roach                'recursion' => $recursion,
2399b5537c3SGreg Roach                'ancestors' => $ancestors,
2409b5537c3SGreg Roach            ]);
2413dcc812bSGreg Roach        } else {
2423dcc812bSGreg Roach            $title = I18N::translate('Relationships');
2433dcc812bSGreg Roach
244f866a2aeSGreg Roach            $ajax_url = '';
2453dcc812bSGreg Roach        }
2469b5537c3SGreg Roach
2479b5537c3SGreg Roach        return $this->viewResponse('modules/relationships-chart/page', [
2489b5537c3SGreg Roach            'ajax_url'          => $ajax_url,
2499b5537c3SGreg Roach            'ancestors'         => $ancestors,
2509b5537c3SGreg Roach            'ancestors_only'    => $ancestors_only,
2519b5537c3SGreg Roach            'ancestors_options' => $this->ancestorsOptions(),
2529b5537c3SGreg Roach            'individual1'       => $individual1,
2539b5537c3SGreg Roach            'individual2'       => $individual2,
2549b5537c3SGreg Roach            'max_recursion'     => $max_recursion,
2559b5537c3SGreg Roach            'module_name'       => $this->name(),
2569b5537c3SGreg Roach            'recursion'         => $recursion,
2579b5537c3SGreg Roach            'recursion_options' => $this->recursionOptions($max_recursion),
2589b5537c3SGreg Roach            'title'             => $title,
2599b5537c3SGreg Roach        ]);
2609b5537c3SGreg Roach    }
2619b5537c3SGreg Roach
2629b5537c3SGreg Roach    /**
2633dcc812bSGreg Roach     * @param Individual $individual1
2643dcc812bSGreg Roach     * @param Individual $individual2
2653dcc812bSGreg Roach     * @param int        $recursion
2663dcc812bSGreg Roach     * @param int        $ancestors
2679b5537c3SGreg Roach     *
2689b5537c3SGreg Roach     * @return Response
2699b5537c3SGreg Roach     */
2703dcc812bSGreg Roach    public function chart(Individual $individual1, Individual $individual2, int $recursion, int $ancestors): Response
2719b5537c3SGreg Roach    {
2723dcc812bSGreg Roach        $tree = $individual1->tree();
2739b5537c3SGreg Roach
2743dcc812bSGreg Roach        $max_recursion = (int) $tree->getPreference('RELATIONSHIP_RECURSION', static::DEFAULT_RECURSION);
2759b5537c3SGreg Roach
2769b5537c3SGreg Roach        $recursion = min($recursion, $max_recursion);
2779b5537c3SGreg Roach
2789b5537c3SGreg Roach        $paths = $this->calculateRelationships($individual1, $individual2, $recursion, (bool) $ancestors);
2799b5537c3SGreg Roach
2809b5537c3SGreg Roach        // @TODO - convert to views
2819b5537c3SGreg Roach        ob_start();
2829b5537c3SGreg Roach        if (I18N::direction() === 'ltr') {
2839b5537c3SGreg Roach            $diagonal1 = Theme::theme()->parameter('image-dline');
2849b5537c3SGreg Roach            $diagonal2 = Theme::theme()->parameter('image-dline2');
2859b5537c3SGreg Roach        } else {
2869b5537c3SGreg Roach            $diagonal1 = Theme::theme()->parameter('image-dline2');
2879b5537c3SGreg Roach            $diagonal2 = Theme::theme()->parameter('image-dline');
2889b5537c3SGreg Roach        }
2899b5537c3SGreg Roach
2909b5537c3SGreg Roach        $num_paths = 0;
2919b5537c3SGreg Roach        foreach ($paths as $path) {
2929b5537c3SGreg Roach            // Extract the relationship names between pairs of individuals
2939b5537c3SGreg Roach            $relationships = $this->oldStyleRelationshipPath($tree, $path);
2949b5537c3SGreg Roach            if (empty($relationships)) {
2959b5537c3SGreg Roach                // Cannot see one of the families/individuals, due to privacy;
2969b5537c3SGreg Roach                continue;
2979b5537c3SGreg Roach            }
2989b5537c3SGreg Roach            echo '<h3>', I18N::translate('Relationship: %s', Functions::getRelationshipNameFromPath(implode('', $relationships), $individual1, $individual2)), '</h3>';
2999b5537c3SGreg Roach            $num_paths++;
3009b5537c3SGreg Roach
3019b5537c3SGreg Roach            // Use a table/grid for layout.
3029b5537c3SGreg Roach            $table = [];
3039b5537c3SGreg Roach            // Current position in the grid.
3049b5537c3SGreg Roach            $x = 0;
3059b5537c3SGreg Roach            $y = 0;
3069b5537c3SGreg Roach            // Extent of the grid.
3079b5537c3SGreg Roach            $min_y = 0;
3089b5537c3SGreg Roach            $max_y = 0;
3099b5537c3SGreg Roach            $max_x = 0;
3109b5537c3SGreg Roach            // For each node in the path.
3119b5537c3SGreg Roach            foreach ($path as $n => $xref) {
3129b5537c3SGreg Roach                if ($n % 2 === 1) {
3139b5537c3SGreg Roach                    switch ($relationships[$n]) {
3149b5537c3SGreg Roach                        case 'hus':
3159b5537c3SGreg Roach                        case 'wif':
3169b5537c3SGreg Roach                        case 'spo':
3179b5537c3SGreg Roach                        case 'bro':
3189b5537c3SGreg Roach                        case 'sis':
3199b5537c3SGreg Roach                        case 'sib':
320*d993d560SGreg Roach                            $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;">' . view('icons/arrow-end') . '</div></div>';
3219b5537c3SGreg Roach                            $x                 += 2;
3229b5537c3SGreg Roach                            break;
3239b5537c3SGreg Roach                        case 'son':
3249b5537c3SGreg Roach                        case 'dau':
3259b5537c3SGreg Roach                        case 'chi':
3269b5537c3SGreg Roach                            if ($n > 2 && preg_match('/fat|mot|par/', $relationships[$n - 2])) {
327*d993d560SGreg 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>';
3289b5537c3SGreg Roach                                $x                     += 2;
3299b5537c3SGreg Roach                            } else {
3309b5537c3SGreg Roach                                $table[$x][$y - 1] = '<div style="background:url(' . Theme::theme()
331*d993d560SGreg 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>';
3329b5537c3SGreg Roach                            }
3339b5537c3SGreg Roach                            $y -= 2;
3349b5537c3SGreg Roach                            break;
3359b5537c3SGreg Roach                        case 'fat':
3369b5537c3SGreg Roach                        case 'mot':
3379b5537c3SGreg Roach                        case 'par':
3389b5537c3SGreg Roach                            if ($n > 2 && preg_match('/son|dau|chi/', $relationships[$n - 2])) {
339*d993d560SGreg 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>';
3409b5537c3SGreg Roach                                $x                     += 2;
3419b5537c3SGreg Roach                            } else {
3429b5537c3SGreg Roach                                $table[$x][$y + 1] = '<div style="background:url(' . Theme::theme()
343*d993d560SGreg 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>';
3449b5537c3SGreg Roach                            }
3459b5537c3SGreg Roach                            $y += 2;
3469b5537c3SGreg Roach                            break;
3479b5537c3SGreg Roach                    }
3489b5537c3SGreg Roach                    $max_x = max($max_x, $x);
3499b5537c3SGreg Roach                    $min_y = min($min_y, $y);
3509b5537c3SGreg Roach                    $max_y = max($max_y, $y);
3519b5537c3SGreg Roach                } else {
3529b5537c3SGreg Roach                    $individual    = Individual::getInstance($xref, $tree);
3539b5537c3SGreg Roach                    $table[$x][$y] = FunctionsPrint::printPedigreePerson($individual);
3549b5537c3SGreg Roach                }
3559b5537c3SGreg Roach            }
3569b5537c3SGreg Roach            echo '<div class="wt-chart wt-relationship-chart">';
3579b5537c3SGreg Roach            echo '<table style="border-collapse: collapse; margin: 20px 50px;">';
3589b5537c3SGreg Roach            for ($y = $max_y; $y >= $min_y; --$y) {
3599b5537c3SGreg Roach                echo '<tr>';
3609b5537c3SGreg Roach                for ($x = 0; $x <= $max_x; ++$x) {
3619b5537c3SGreg Roach                    echo '<td style="padding: 0;">';
3629b5537c3SGreg Roach                    if (isset($table[$x][$y])) {
3639b5537c3SGreg Roach                        echo $table[$x][$y];
3649b5537c3SGreg Roach                    }
3659b5537c3SGreg Roach                    echo '</td>';
3669b5537c3SGreg Roach                }
3679b5537c3SGreg Roach                echo '</tr>';
3689b5537c3SGreg Roach            }
3699b5537c3SGreg Roach            echo '</table>';
3709b5537c3SGreg Roach            echo '</div>';
3719b5537c3SGreg Roach        }
3729b5537c3SGreg Roach
3739b5537c3SGreg Roach        if (!$num_paths) {
3749b5537c3SGreg Roach            echo '<p>', I18N::translate('No link between the two individuals could be found.'), '</p>';
3759b5537c3SGreg Roach        }
3769b5537c3SGreg Roach
3779b5537c3SGreg Roach        $html = ob_get_clean();
3789b5537c3SGreg Roach
3799b5537c3SGreg Roach        return new Response($html);
3809b5537c3SGreg Roach    }
3819b5537c3SGreg Roach
3829b5537c3SGreg Roach    /**
3839b5537c3SGreg Roach     * Calculate the shortest paths - or all paths - between two individuals.
3849b5537c3SGreg Roach     *
3859b5537c3SGreg Roach     * @param Individual $individual1
3869b5537c3SGreg Roach     * @param Individual $individual2
3879b5537c3SGreg Roach     * @param int        $recursion How many levels of recursion to use
3889b5537c3SGreg Roach     * @param bool       $ancestor  Restrict to relationships via a common ancestor
3899b5537c3SGreg Roach     *
3909b5537c3SGreg Roach     * @return string[][]
3919b5537c3SGreg Roach     */
3929b5537c3SGreg Roach    private function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false): array
3939b5537c3SGreg Roach    {
3943dcc812bSGreg Roach        $tree = $individual1->tree();
3953dcc812bSGreg Roach
3969b5537c3SGreg Roach        $rows = DB::table('link')
3973dcc812bSGreg Roach            ->where('l_file', '=', $tree->id())
3989b5537c3SGreg Roach            ->whereIn('l_type', ['FAMS', 'FAMC'])
3999b5537c3SGreg Roach            ->select(['l_from', 'l_to'])
4009b5537c3SGreg Roach            ->get();
4019b5537c3SGreg Roach
4029b5537c3SGreg Roach        // Optionally restrict the graph to the ancestors of the individuals.
4039b5537c3SGreg Roach        if ($ancestor) {
4043dcc812bSGreg Roach            $ancestors = $this->allAncestors($individual1->xref(), $individual2->xref(), $tree->id());
4053dcc812bSGreg Roach            $exclude   = $this->excludeFamilies($individual1->xref(), $individual2->xref(), $tree->id());
4069b5537c3SGreg Roach        } else {
4079b5537c3SGreg Roach            $ancestors = [];
4089b5537c3SGreg Roach            $exclude   = [];
4099b5537c3SGreg Roach        }
4109b5537c3SGreg Roach
4119b5537c3SGreg Roach        $graph = [];
4129b5537c3SGreg Roach
4139b5537c3SGreg Roach        foreach ($rows as $row) {
4149b5537c3SGreg Roach            if (empty($ancestors) || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) {
4159b5537c3SGreg Roach                $graph[$row->l_from][$row->l_to] = 1;
4169b5537c3SGreg Roach                $graph[$row->l_to][$row->l_from] = 1;
4179b5537c3SGreg Roach            }
4189b5537c3SGreg Roach        }
4199b5537c3SGreg Roach
4209b5537c3SGreg Roach        $xref1    = $individual1->xref();
4219b5537c3SGreg Roach        $xref2    = $individual2->xref();
4229b5537c3SGreg Roach        $dijkstra = new Dijkstra($graph);
4239b5537c3SGreg Roach        $paths    = $dijkstra->shortestPaths($xref1, $xref2);
4249b5537c3SGreg Roach
4259b5537c3SGreg Roach        // Only process each exclusion list once;
4269b5537c3SGreg Roach        $excluded = [];
4279b5537c3SGreg Roach
4289b5537c3SGreg Roach        $queue = [];
4299b5537c3SGreg Roach        foreach ($paths as $path) {
4309b5537c3SGreg Roach            // Insert the paths into the queue, with an exclusion list.
4319b5537c3SGreg Roach            $queue[] = [
4329b5537c3SGreg Roach                'path'    => $path,
4339b5537c3SGreg Roach                'exclude' => [],
4349b5537c3SGreg Roach            ];
4359b5537c3SGreg Roach            // While there are un-extended paths
4369b5537c3SGreg Roach            for ($next = current($queue); $next !== false; $next = next($queue)) {
4379b5537c3SGreg Roach                // For each family on the path
4389b5537c3SGreg Roach                for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) {
4399b5537c3SGreg Roach                    $exclude = $next['exclude'];
4409b5537c3SGreg Roach                    if (count($exclude) >= $recursion) {
4419b5537c3SGreg Roach                        continue;
4429b5537c3SGreg Roach                    }
4439b5537c3SGreg Roach                    $exclude[] = $next['path'][$n];
4449b5537c3SGreg Roach                    sort($exclude);
4459b5537c3SGreg Roach                    $tmp = implode('-', $exclude);
4469b5537c3SGreg Roach                    if (in_array($tmp, $excluded)) {
4479b5537c3SGreg Roach                        continue;
4489b5537c3SGreg Roach                    }
4499b5537c3SGreg Roach
4509b5537c3SGreg Roach                    $excluded[] = $tmp;
4519b5537c3SGreg Roach                    // Add any new path to the queue
4529b5537c3SGreg Roach                    foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) {
4539b5537c3SGreg Roach                        $queue[] = [
4549b5537c3SGreg Roach                            'path'    => $new_path,
4559b5537c3SGreg Roach                            'exclude' => $exclude,
4569b5537c3SGreg Roach                        ];
4579b5537c3SGreg Roach                    }
4589b5537c3SGreg Roach                }
4599b5537c3SGreg Roach            }
4609b5537c3SGreg Roach        }
4619b5537c3SGreg Roach        // Extract the paths from the queue, removing duplicates.
4629b5537c3SGreg Roach        $paths = [];
4639b5537c3SGreg Roach        foreach ($queue as $next) {
4649b5537c3SGreg Roach            $paths[implode('-', $next['path'])] = $next['path'];
4659b5537c3SGreg Roach        }
4669b5537c3SGreg Roach
4679b5537c3SGreg Roach        return $paths;
4689b5537c3SGreg Roach    }
4699b5537c3SGreg Roach
4709b5537c3SGreg Roach    /**
4719b5537c3SGreg Roach     * Convert a path (list of XREFs) to an "old-style" string of relationships.
4729b5537c3SGreg Roach     * Return an empty array, if privacy rules prevent us viewing any node.
4739b5537c3SGreg Roach     *
4749b5537c3SGreg Roach     * @param Tree     $tree
4759b5537c3SGreg Roach     * @param string[] $path Alternately Individual / Family
4769b5537c3SGreg Roach     *
4779b5537c3SGreg Roach     * @return string[]
4789b5537c3SGreg Roach     */
4799b5537c3SGreg Roach    private function oldStyleRelationshipPath(Tree $tree, array $path): array
4809b5537c3SGreg Roach    {
4819b5537c3SGreg Roach        $spouse_codes  = [
4829b5537c3SGreg Roach            'M' => 'hus',
4839b5537c3SGreg Roach            'F' => 'wif',
4849b5537c3SGreg Roach            'U' => 'spo',
4859b5537c3SGreg Roach        ];
4869b5537c3SGreg Roach        $parent_codes  = [
4879b5537c3SGreg Roach            'M' => 'fat',
4889b5537c3SGreg Roach            'F' => 'mot',
4899b5537c3SGreg Roach            'U' => 'par',
4909b5537c3SGreg Roach        ];
4919b5537c3SGreg Roach        $child_codes   = [
4929b5537c3SGreg Roach            'M' => 'son',
4939b5537c3SGreg Roach            'F' => 'dau',
4949b5537c3SGreg Roach            'U' => 'chi',
4959b5537c3SGreg Roach        ];
4969b5537c3SGreg Roach        $sibling_codes = [
4979b5537c3SGreg Roach            'M' => 'bro',
4989b5537c3SGreg Roach            'F' => 'sis',
4999b5537c3SGreg Roach            'U' => 'sib',
5009b5537c3SGreg Roach        ];
5019b5537c3SGreg Roach        $relationships = [];
5029b5537c3SGreg Roach
5039b5537c3SGreg Roach        for ($i = 1, $count = count($path); $i < $count; $i += 2) {
5049b5537c3SGreg Roach            $family = Family::getInstance($path[$i], $tree);
5059b5537c3SGreg Roach            $prev   = Individual::getInstance($path[$i - 1], $tree);
5069b5537c3SGreg Roach            $next   = Individual::getInstance($path[$i + 1], $tree);
5079b5537c3SGreg Roach            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->xref() . '@/', $family->gedcom(), $match)) {
5089b5537c3SGreg Roach                $rel1 = $match[1];
5099b5537c3SGreg Roach            } else {
5109b5537c3SGreg Roach                return [];
5119b5537c3SGreg Roach            }
5129b5537c3SGreg Roach            if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->xref() . '@/', $family->gedcom(), $match)) {
5139b5537c3SGreg Roach                $rel2 = $match[1];
5149b5537c3SGreg Roach            } else {
5159b5537c3SGreg Roach                return [];
5169b5537c3SGreg Roach            }
5179b5537c3SGreg Roach            if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
5189b5537c3SGreg Roach                $relationships[$i] = $spouse_codes[$next->getSex()];
5199b5537c3SGreg Roach            } elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') {
5209b5537c3SGreg Roach                $relationships[$i] = $child_codes[$next->getSex()];
5219b5537c3SGreg Roach            } elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
5229b5537c3SGreg Roach                $relationships[$i] = $parent_codes[$next->getSex()];
5239b5537c3SGreg Roach            } elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') {
5249b5537c3SGreg Roach                $relationships[$i] = $sibling_codes[$next->getSex()];
5259b5537c3SGreg Roach            }
5269b5537c3SGreg Roach        }
5279b5537c3SGreg Roach
5289b5537c3SGreg Roach        return $relationships;
5299b5537c3SGreg Roach    }
5309b5537c3SGreg Roach
5319b5537c3SGreg Roach    /**
5329b5537c3SGreg Roach     * Find all ancestors of a list of individuals
5339b5537c3SGreg Roach     *
5349b5537c3SGreg Roach     * @param string $xref1
5359b5537c3SGreg Roach     * @param string $xref2
5369b5537c3SGreg Roach     * @param int    $tree_id
5379b5537c3SGreg Roach     *
5389b5537c3SGreg Roach     * @return string[]
5399b5537c3SGreg Roach     */
5409b5537c3SGreg Roach    private function allAncestors($xref1, $xref2, $tree_id): array
5419b5537c3SGreg Roach    {
5429b5537c3SGreg Roach        $ancestors = [
5439b5537c3SGreg Roach            $xref1,
5449b5537c3SGreg Roach            $xref2,
5459b5537c3SGreg Roach        ];
5469b5537c3SGreg Roach
5479b5537c3SGreg Roach        $queue = [
5489b5537c3SGreg Roach            $xref1,
5499b5537c3SGreg Roach            $xref2,
5509b5537c3SGreg Roach        ];
5519b5537c3SGreg Roach        while (!empty($queue)) {
5529b5537c3SGreg Roach            $parents = DB::table('link AS l1')
5539b5537c3SGreg Roach                ->join('link AS l2', function (JoinClause $join): void {
5549b5537c3SGreg Roach                    $join
5559b5537c3SGreg Roach                        ->on('l1.l_to', '=', 'l2.l_to')
5569b5537c3SGreg Roach                        ->on('l1.l_file', '=', 'l2.l_file');
5579b5537c3SGreg Roach                })
5589b5537c3SGreg Roach                ->where('l1.l_file', '=', $tree_id)
5599b5537c3SGreg Roach                ->where('l1.l_type', '=', 'FAMC')
5609b5537c3SGreg Roach                ->where('l2.l_type', '=', 'FAMS')
5619b5537c3SGreg Roach                ->whereIn('l1.l_from', $queue)
5629b5537c3SGreg Roach                ->pluck('l2.l_from');
5639b5537c3SGreg Roach
5649b5537c3SGreg Roach            $queue = [];
5659b5537c3SGreg Roach            foreach ($parents as $parent) {
5669b5537c3SGreg Roach                if (!in_array($parent, $ancestors)) {
5679b5537c3SGreg Roach                    $ancestors[] = $parent;
5689b5537c3SGreg Roach                    $queue[]     = $parent;
5699b5537c3SGreg Roach                }
5709b5537c3SGreg Roach            }
5719b5537c3SGreg Roach        }
5729b5537c3SGreg Roach
5739b5537c3SGreg Roach        return $ancestors;
5749b5537c3SGreg Roach    }
5759b5537c3SGreg Roach
5769b5537c3SGreg Roach    /**
5779b5537c3SGreg Roach     * Find all families of two individuals
5789b5537c3SGreg Roach     *
5799b5537c3SGreg Roach     * @param string $xref1
5809b5537c3SGreg Roach     * @param string $xref2
5819b5537c3SGreg Roach     * @param int    $tree_id
5829b5537c3SGreg Roach     *
5839b5537c3SGreg Roach     * @return string[]
5849b5537c3SGreg Roach     */
5859b5537c3SGreg Roach    private function excludeFamilies($xref1, $xref2, $tree_id): array
5869b5537c3SGreg Roach    {
5879b5537c3SGreg Roach        return DB::table('link AS l1')
5889b5537c3SGreg Roach            ->join('link AS l2', function (JoinClause $join): void {
5899b5537c3SGreg Roach                $join
5909b5537c3SGreg Roach                    ->on('l1.l_to', '=', 'l2.l_to')
5919b5537c3SGreg Roach                    ->on('l1.l_type', '=', 'l2.l_type')
5929b5537c3SGreg Roach                    ->on('l1.l_file', '=', 'l2.l_file');
5939b5537c3SGreg Roach            })
5949b5537c3SGreg Roach            ->where('l1.l_file', '=', $tree_id)
5959b5537c3SGreg Roach            ->where('l1.l_type', '=', 'FAMS')
5969b5537c3SGreg Roach            ->where('l1.l_from', '=', $xref1)
5979b5537c3SGreg Roach            ->where('l2.l_from', '=', $xref2)
5989b5537c3SGreg Roach            ->pluck('l1.l_to')
5999b5537c3SGreg Roach            ->all();
6009b5537c3SGreg Roach    }
6019b5537c3SGreg Roach
6029b5537c3SGreg Roach    /**
6039b5537c3SGreg Roach     * Possible options for the recursion option
6049b5537c3SGreg Roach     *
6059b5537c3SGreg Roach     * @param int $max_recursion
6069b5537c3SGreg Roach     *
6079b5537c3SGreg Roach     * @return array
6089b5537c3SGreg Roach     */
6099b5537c3SGreg Roach    private function recursionOptions(int $max_recursion): array
6109b5537c3SGreg Roach    {
6113dcc812bSGreg Roach        if ($max_recursion === static::UNLIMITED_RECURSION) {
6129b5537c3SGreg Roach            $text = I18N::translate('Find all possible relationships');
6139b5537c3SGreg Roach        } else {
6149b5537c3SGreg Roach            $text = I18N::translate('Find other relationships');
6159b5537c3SGreg Roach        }
6169b5537c3SGreg Roach
6179b5537c3SGreg Roach        return [
6189b5537c3SGreg Roach            '0'            => I18N::translate('Find the closest relationships'),
6199b5537c3SGreg Roach            $max_recursion => $text,
6209b5537c3SGreg Roach        ];
6219b5537c3SGreg Roach    }
622168ff6f3Sric2016}
623