1168ff6f3Sric2016<?php 23976b470SGreg Roach 3168ff6f3Sric2016/** 4168ff6f3Sric2016 * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 6168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify 7168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by 8168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or 9168ff6f3Sric2016 * (at your option) any later version. 10168ff6f3Sric2016 * This program is distributed in the hope that it will be useful, 11168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13168ff6f3Sric2016 * GNU General Public License for more details. 14168ff6f3Sric2016 * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16168ff6f3Sric2016 */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 21168ff6f3Sric2016 2271378461SGreg Roachuse Fig\Http\Message\RequestMethodInterface; 23241a1636SGreg Roachuse Fisharebest\Webtrees\Auth; 24168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 25168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 261f918143SScrutinizer Auto-Fixeruse Fisharebest\Webtrees\Menu; 272f955fcfSGreg Roachuse Fisharebest\Webtrees\Registry; 28241a1636SGreg Roachuse Fisharebest\Webtrees\Services\ChartService; 29b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 306ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 316ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 3271378461SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 3371378461SGreg Roach 3471378461SGreg Roachuse function route; 3571378461SGreg Roachuse function view; 36168ff6f3Sric2016 37168ff6f3Sric2016/** 38168ff6f3Sric2016 * Class PedigreeChartModule 39168ff6f3Sric2016 */ 4071378461SGreg Roachclass PedigreeChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface 41c1010edaSGreg Roach{ 4249a243cbSGreg Roach use ModuleChartTrait; 4349a243cbSGreg Roach 4472f04adfSGreg Roach protected const ROUTE_URL = '/tree/{tree}/pedigree-{style}-{generations}/{xref}'; 4571378461SGreg Roach 4671378461SGreg Roach // Chart styles 4771378461SGreg Roach public const STYLE_LEFT = 'left'; 4871378461SGreg Roach public const STYLE_RIGHT = 'right'; 4971378461SGreg Roach public const STYLE_UP = 'up'; 5071378461SGreg Roach public const STYLE_DOWN = 'down'; 5171378461SGreg Roach 52e759aebbSGreg Roach // Defaults 53266e9c61SGreg Roach public const DEFAULT_GENERATIONS = '4'; 54266e9c61SGreg Roach public const DEFAULT_STYLE = self::STYLE_RIGHT; 5571378461SGreg Roach protected const DEFAULT_PARAMETERS = [ 5671378461SGreg Roach 'generations' => self::DEFAULT_GENERATIONS, 5771378461SGreg Roach 'style' => self::DEFAULT_STYLE, 58e759aebbSGreg Roach ]; 59241a1636SGreg Roach 6071378461SGreg Roach // Limits 6171378461SGreg Roach protected const MINIMUM_GENERATIONS = 2; 6271378461SGreg Roach protected const MAXIMUM_GENERATIONS = 12; 6371378461SGreg Roach 6471378461SGreg Roach // For RTL languages 6571378461SGreg Roach protected const MIRROR_STYLE = [ 6671378461SGreg Roach self::STYLE_UP => self::STYLE_DOWN, 6771378461SGreg Roach self::STYLE_DOWN => self::STYLE_UP, 6871378461SGreg Roach self::STYLE_LEFT => self::STYLE_RIGHT, 6971378461SGreg Roach self::STYLE_RIGHT => self::STYLE_LEFT, 7071378461SGreg Roach ]; 71241a1636SGreg Roach 7243f2f523SGreg Roach private ChartService $chart_service; 7357ab2231SGreg Roach 7457ab2231SGreg Roach /** 7557ab2231SGreg Roach * @param ChartService $chart_service 7657ab2231SGreg Roach */ 773976b470SGreg Roach public function __construct(ChartService $chart_service) 783976b470SGreg Roach { 7957ab2231SGreg Roach $this->chart_service = $chart_service; 8057ab2231SGreg Roach } 8157ab2231SGreg Roach 82168ff6f3Sric2016 /** 8371378461SGreg Roach * Initialization. 8471378461SGreg Roach * 859e18e23bSGreg Roach * @return void 8671378461SGreg Roach */ 879e18e23bSGreg Roach public function boot(): void 8871378461SGreg Roach { 89158900c2SGreg Roach Registry::routeFactory()->routeMap() 9072f04adfSGreg Roach ->get(static::class, static::ROUTE_URL, $this) 91158900c2SGreg Roach ->allows(RequestMethodInterface::METHOD_POST); 9271378461SGreg Roach } 9371378461SGreg Roach 9471378461SGreg Roach /** 950cfd6963SGreg Roach * How should this module be identified in the control panel, etc.? 96168ff6f3Sric2016 * 97168ff6f3Sric2016 * @return string 98168ff6f3Sric2016 */ 9949a243cbSGreg Roach public function title(): string 100c1010edaSGreg Roach { 101bbb76c12SGreg Roach /* I18N: Name of a module/chart */ 102bbb76c12SGreg Roach return I18N::translate('Pedigree'); 103168ff6f3Sric2016 } 104168ff6f3Sric2016 10549a243cbSGreg Roach public function description(): string 106c1010edaSGreg Roach { 107bbb76c12SGreg Roach /* I18N: Description of the “PedigreeChart” module */ 108bbb76c12SGreg Roach return I18N::translate('A chart of an individual’s ancestors, formatted as a tree.'); 109168ff6f3Sric2016 } 110168ff6f3Sric2016 111168ff6f3Sric2016 /** 112377a2979SGreg Roach * CSS class for the URL. 113377a2979SGreg Roach * 114377a2979SGreg Roach * @return string 115377a2979SGreg Roach */ 116377a2979SGreg Roach public function chartMenuClass(): string 117377a2979SGreg Roach { 118377a2979SGreg Roach return 'menu-chart-pedigree'; 119377a2979SGreg Roach } 120377a2979SGreg Roach 121377a2979SGreg Roach /** 1224eb71cfaSGreg Roach * Return a menu item for this chart - for use in individual boxes. 1234eb71cfaSGreg Roach * 12460bc3e3fSGreg Roach * @param Individual $individual 12560bc3e3fSGreg Roach * 1264eb71cfaSGreg Roach * @return Menu|null 1274eb71cfaSGreg Roach */ 128*1ff45046SGreg Roach public function chartBoxMenu(Individual $individual): Menu|null 129c1010edaSGreg Roach { 130e6562982SGreg Roach return $this->chartMenu($individual); 131e6562982SGreg Roach } 132e6562982SGreg Roach 133e6562982SGreg Roach /** 134e6562982SGreg Roach * The title for a specific instance of this chart. 135e6562982SGreg Roach * 136e6562982SGreg Roach * @param Individual $individual 137e6562982SGreg Roach * 138e6562982SGreg Roach * @return string 139e6562982SGreg Roach */ 140e6562982SGreg Roach public function chartTitle(Individual $individual): string 141e6562982SGreg Roach { 142e6562982SGreg Roach /* I18N: %s is an individual’s name */ 14339ca88baSGreg Roach return I18N::translate('Pedigree tree of %s', $individual->fullName()); 144e6562982SGreg Roach } 145e6562982SGreg Roach 146e6562982SGreg Roach /** 14771378461SGreg Roach * The URL for a page showing chart options. 148e6562982SGreg Roach * 14971378461SGreg Roach * @param Individual $individual 15076d39c55SGreg Roach * @param array<bool|int|string|array<string>|null> $parameters 15171378461SGreg Roach * 15271378461SGreg Roach * @return string 15371378461SGreg Roach */ 15471378461SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 15571378461SGreg Roach { 15672f04adfSGreg Roach return route(static::class, [ 15771378461SGreg Roach 'xref' => $individual->xref(), 15871378461SGreg Roach 'tree' => $individual->tree()->name(), 15972f04adfSGreg Roach ] + $parameters + static::DEFAULT_PARAMETERS); 16071378461SGreg Roach } 16171378461SGreg Roach 16271378461SGreg Roach /** 1636ccdf4f0SGreg Roach * @param ServerRequestInterface $request 164241a1636SGreg Roach * 1656ccdf4f0SGreg Roach * @return ResponseInterface 166241a1636SGreg Roach */ 16771378461SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 168241a1636SGreg Roach { 169b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 170b55cbc6bSGreg Roach $user = Validator::attributes($request)->user(); 171158900c2SGreg Roach $xref = Validator::attributes($request)->isXref()->string('xref'); 172158900c2SGreg Roach $style = Validator::attributes($request)->isInArrayKeys($this->styles('ltr'))->string('style'); 173158900c2SGreg Roach $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'); 174158900c2SGreg Roach $ajax = Validator::queryParams($request)->boolean('ajax', false); 175241a1636SGreg Roach 17671378461SGreg Roach // Convert POST requests into GET requests for pretty URLs. 17771378461SGreg Roach if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 17872f04adfSGreg Roach return redirect(route(self::class, [ 179b55cbc6bSGreg Roach 'tree' => $tree->name(), 180158900c2SGreg Roach 'xref' => Validator::parsedBody($request)->isXref()->string('xref'), 181158900c2SGreg Roach 'style' => Validator::parsedBody($request)->isInArrayKeys($this->styles('ltr'))->string('style'), 182158900c2SGreg Roach 'generations' => Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'), 18371378461SGreg Roach ])); 18471378461SGreg Roach } 18571378461SGreg Roach 186ef483801SGreg Roach Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); 187241a1636SGreg Roach 188b55cbc6bSGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 189b55cbc6bSGreg Roach $individual = Auth::checkIndividualAccess($individual, false, true); 190241a1636SGreg Roach 191b55cbc6bSGreg Roach if ($ajax) { 19271378461SGreg Roach $this->layout = 'layouts/ajax'; 19371378461SGreg Roach 19471378461SGreg Roach $ancestors = $this->chart_service->sosaStradonitzAncestors($individual, $generations); 19571378461SGreg Roach 19671378461SGreg Roach // Father’s ancestors link to the father’s pedigree 19771378461SGreg Roach // Mother’s ancestors link to the mother’s pedigree.. 198*1ff45046SGreg Roach $links = $ancestors->map(function (Individual|null $individual, $sosa) use ($ancestors, $style, $generations): string { 19971378461SGreg Roach if ($individual instanceof Individual && $sosa >= 2 ** $generations / 2 && $individual->childFamilies()->isNotEmpty()) { 20071378461SGreg Roach // The last row/column, and there are more generations. 20171378461SGreg Roach if ($sosa >= 2 ** $generations * 3 / 4) { 20271378461SGreg Roach return $this->nextLink($ancestors->get(3), $style, $generations); 20371378461SGreg Roach } 20471378461SGreg Roach 20571378461SGreg Roach return $this->nextLink($ancestors->get(2), $style, $generations); 20671378461SGreg Roach } 20771378461SGreg Roach 20871378461SGreg Roach // A spacer to fix the "Left" layout. 20971378461SGreg Roach return '<span class="invisible px-2">' . view('icons/arrow-' . $style) . '</span>'; 21071378461SGreg Roach }); 21171378461SGreg Roach 21271378461SGreg Roach // Root individual links to their children. 21371378461SGreg Roach $links->put(1, $this->previousLink($individual, $style, $generations)); 21471378461SGreg Roach 21571378461SGreg Roach return $this->viewResponse('modules/pedigree-chart/chart', [ 21671378461SGreg Roach 'ancestors' => $ancestors, 21771378461SGreg Roach 'generations' => $generations, 21871378461SGreg Roach 'style' => $style, 21971378461SGreg Roach 'layout' => 'right', 22071378461SGreg Roach 'links' => $links, 221bb1ec7dcSGreg Roach 'spacer' => $this->spacer(), 22271378461SGreg Roach ]); 223241a1636SGreg Roach } 224241a1636SGreg Roach 225241a1636SGreg Roach $ajax_url = $this->chartUrl($individual, [ 2269b5537c3SGreg Roach 'ajax' => true, 227241a1636SGreg Roach 'generations' => $generations, 22871378461SGreg Roach 'style' => $style, 22971378461SGreg Roach 'xref' => $xref, 230241a1636SGreg Roach ]); 231241a1636SGreg Roach 2329b5537c3SGreg Roach return $this->viewResponse('modules/pedigree-chart/page', [ 233241a1636SGreg Roach 'ajax_url' => $ajax_url, 234241a1636SGreg Roach 'generations' => $generations, 235241a1636SGreg Roach 'individual' => $individual, 23671378461SGreg Roach 'module' => $this->name(), 237f91b18ebSGreg Roach 'max_generations' => self::MAXIMUM_GENERATIONS, 238f91b18ebSGreg Roach 'min_generations' => self::MINIMUM_GENERATIONS, 23971378461SGreg Roach 'style' => $style, 2402f955fcfSGreg Roach 'styles' => $this->styles(I18N::direction()), 241241a1636SGreg Roach 'title' => $this->chartTitle($individual), 242ef5d23f1SGreg Roach 'tree' => $tree, 243241a1636SGreg Roach ]); 244241a1636SGreg Roach } 245241a1636SGreg Roach 246241a1636SGreg Roach /** 247bb1ec7dcSGreg Roach * A link-sized spacer, to maintain the chart layout 248bb1ec7dcSGreg Roach * 249bb1ec7dcSGreg Roach * @return string 250bb1ec7dcSGreg Roach */ 251bb1ec7dcSGreg Roach public function spacer(): string 252bb1ec7dcSGreg Roach { 253bb1ec7dcSGreg Roach return '<span class="px-2">' . view('icons/spacer') . '</span>'; 254bb1ec7dcSGreg Roach } 255bb1ec7dcSGreg Roach 256bb1ec7dcSGreg Roach /** 257241a1636SGreg Roach * Build a menu for the chart root individual 258241a1636SGreg Roach * 259e759aebbSGreg Roach * @param Individual $individual 26071378461SGreg Roach * @param string $style 261241a1636SGreg Roach * @param int $generations 262e6562982SGreg Roach * 263e6562982SGreg Roach * @return string 264e6562982SGreg Roach */ 26571378461SGreg Roach public function nextLink(Individual $individual, string $style, int $generations): string 266e6562982SGreg Roach { 26771378461SGreg Roach $icon = view('icons/arrow-' . $style); 268e759aebbSGreg Roach $title = $this->chartTitle($individual); 269e759aebbSGreg Roach $url = $this->chartUrl($individual, [ 27071378461SGreg Roach 'style' => $style, 271e759aebbSGreg Roach 'generations' => $generations, 272e759aebbSGreg Roach ]); 273241a1636SGreg Roach 274315eb316SGreg Roach return '<a class="px-2" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $icon . '<span class="visually-hidden">' . $title . '</span></a>'; 275241a1636SGreg Roach } 276241a1636SGreg Roach 277241a1636SGreg Roach /** 278e759aebbSGreg Roach * Build a menu for the chart root individual 279241a1636SGreg Roach * 280e759aebbSGreg Roach * @param Individual $individual 28171378461SGreg Roach * @param string $style 282241a1636SGreg Roach * @param int $generations 283241a1636SGreg Roach * 284241a1636SGreg Roach * @return string 285241a1636SGreg Roach */ 28671378461SGreg Roach public function previousLink(Individual $individual, string $style, int $generations): string 287241a1636SGreg Roach { 28871378461SGreg Roach $icon = view('icons/arrow-' . self::MIRROR_STYLE[$style]); 289e759aebbSGreg Roach 290e759aebbSGreg Roach $siblings = []; 291e759aebbSGreg Roach $spouses = []; 292e759aebbSGreg Roach $children = []; 293e759aebbSGreg Roach 29439ca88baSGreg Roach foreach ($individual->childFamilies() as $family) { 29539ca88baSGreg Roach foreach ($family->children() as $child) { 296e759aebbSGreg Roach if ($child !== $individual) { 29771378461SGreg Roach $siblings[] = $this->individualLink($child, $style, $generations); 298241a1636SGreg Roach } 299241a1636SGreg Roach } 300241a1636SGreg Roach } 301241a1636SGreg Roach 30239ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 30339ca88baSGreg Roach foreach ($family->spouses() as $spouse) { 304e759aebbSGreg Roach if ($spouse !== $individual) { 30571378461SGreg Roach $spouses[] = $this->individualLink($spouse, $style, $generations); 306e759aebbSGreg Roach } 307e759aebbSGreg Roach } 308e759aebbSGreg Roach 30939ca88baSGreg Roach foreach ($family->children() as $child) { 31071378461SGreg Roach $children[] = $this->individualLink($child, $style, $generations); 311e759aebbSGreg Roach } 312e759aebbSGreg Roach } 313e759aebbSGreg Roach 314e759aebbSGreg Roach return view('modules/pedigree-chart/previous', [ 315e759aebbSGreg Roach 'icon' => $icon, 316e759aebbSGreg Roach 'individual' => $individual, 317e759aebbSGreg Roach 'generations' => $generations, 31871378461SGreg Roach 'style' => $style, 319e759aebbSGreg Roach 'chart' => $this, 320e759aebbSGreg Roach 'siblings' => $siblings, 321e759aebbSGreg Roach 'spouses' => $spouses, 322e759aebbSGreg Roach 'children' => $children, 323e759aebbSGreg Roach ]); 324e759aebbSGreg Roach } 325e759aebbSGreg Roach 326e759aebbSGreg Roach /** 327e759aebbSGreg Roach * @param Individual $individual 32871378461SGreg Roach * @param string $style 329e759aebbSGreg Roach * @param int $generations 330e759aebbSGreg Roach * 331e759aebbSGreg Roach * @return string 332e759aebbSGreg Roach */ 33371378461SGreg Roach protected function individualLink(Individual $individual, string $style, int $generations): string 334e759aebbSGreg Roach { 33539ca88baSGreg Roach $text = $individual->fullName(); 336e759aebbSGreg Roach $title = $this->chartTitle($individual); 337e759aebbSGreg Roach $url = $this->chartUrl($individual, [ 33871378461SGreg Roach 'style' => $style, 339e759aebbSGreg Roach 'generations' => $generations, 340e759aebbSGreg Roach ]); 341e759aebbSGreg Roach 342e759aebbSGreg Roach return '<a class="dropdown-item" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $text . '</a>'; 343241a1636SGreg Roach } 344241a1636SGreg Roach 345241a1636SGreg Roach /** 34671378461SGreg Roach * This chart can display its output in a number of styles 34771378461SGreg Roach * 3482f955fcfSGreg Roach * @param string $direction 3492f955fcfSGreg Roach * 35024f2a3afSGreg Roach * @return array<string> 351241a1636SGreg Roach */ 3522f955fcfSGreg Roach protected function styles(string $direction): array 353241a1636SGreg Roach { 3542f955fcfSGreg Roach // On right-to-left pages, the CSS will mirror the chart, so we need to mirror the label. 3552f955fcfSGreg Roach if ($direction === 'rtl') { 356241a1636SGreg Roach return [ 3572f955fcfSGreg Roach self::STYLE_RIGHT => view('icons/pedigree-left') . I18N::translate('left'), 3582f955fcfSGreg Roach self::STYLE_LEFT => view('icons/pedigree-right') . I18N::translate('right'), 3592f955fcfSGreg Roach self::STYLE_UP => view('icons/pedigree-up') . I18N::translate('up'), 3602f955fcfSGreg Roach self::STYLE_DOWN => view('icons/pedigree-down') . I18N::translate('down'), 3612f955fcfSGreg Roach ]; 3622f955fcfSGreg Roach } 3632f955fcfSGreg Roach 3642f955fcfSGreg Roach return [ 3652f955fcfSGreg Roach self::STYLE_LEFT => view('icons/pedigree-left') . I18N::translate('left'), 3662f955fcfSGreg Roach self::STYLE_RIGHT => view('icons/pedigree-right') . I18N::translate('right'), 3672f955fcfSGreg Roach self::STYLE_UP => view('icons/pedigree-up') . I18N::translate('up'), 3682f955fcfSGreg Roach self::STYLE_DOWN => view('icons/pedigree-down') . I18N::translate('down'), 369241a1636SGreg Roach ]; 370e6562982SGreg Roach } 371168ff6f3Sric2016} 372