1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Module; 20 21use Aura\Router\RouterContainer; 22use Fig\Http\Message\RequestMethodInterface; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Functions\FunctionsEdit; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Menu; 28use Fisharebest\Webtrees\Services\ChartService; 29use Psr\Http\Message\ResponseInterface; 30use Psr\Http\Message\ServerRequestInterface; 31use Psr\Http\Server\RequestHandlerInterface; 32 33use function max; 34use function min; 35use function route; 36use function view; 37 38/** 39 * Class PedigreeChartModule 40 */ 41class PedigreeChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface 42{ 43 use ModuleChartTrait; 44 45 private const ROUTE_NAME = 'pedigree-chart'; 46 private const ROUTE_URL = '/tree/{tree}/pedigree-{style}-{generations}/{xref}'; 47 48 // Chart styles 49 public const STYLE_LEFT = 'left'; 50 public const STYLE_RIGHT = 'right'; 51 public const STYLE_UP = 'up'; 52 public const STYLE_DOWN = 'down'; 53 54 // Defaults 55 protected const DEFAULT_GENERATIONS = '4'; 56 protected const DEFAULT_STYLE = self::STYLE_RIGHT; 57 protected const DEFAULT_PARAMETERS = [ 58 'generations' => self::DEFAULT_GENERATIONS, 59 'style' => self::DEFAULT_STYLE, 60 ]; 61 62 // Limits 63 protected const MINIMUM_GENERATIONS = 2; 64 protected const MAXIMUM_GENERATIONS = 12; 65 66 // For RTL languages 67 protected const MIRROR_STYLE = [ 68 self::STYLE_UP => self::STYLE_DOWN, 69 self::STYLE_DOWN => self::STYLE_UP, 70 self::STYLE_LEFT => self::STYLE_RIGHT, 71 self::STYLE_RIGHT => self::STYLE_LEFT, 72 ]; 73 74 /** @var ChartService */ 75 private $chart_service; 76 77 /** 78 * PedigreeChartModule constructor. 79 * 80 * @param ChartService $chart_service 81 */ 82 public function __construct(ChartService $chart_service) 83 { 84 $this->chart_service = $chart_service; 85 } 86 87 /** 88 * Initialization. 89 * 90 * @param RouterContainer $router_container 91 */ 92 public function boot(RouterContainer $router_container) 93 { 94 $router_container->getMap() 95 ->get(self::ROUTE_NAME, self::ROUTE_URL, self::class) 96 ->allows(RequestMethodInterface::METHOD_POST) 97 ->tokens([ 98 'generations' => '\d+', 99 'style' => implode('|', array_keys($this->styles())), 100 ]); 101 } 102 103 /** 104 * How should this module be identified in the control panel, etc.? 105 * 106 * @return string 107 */ 108 public function title(): string 109 { 110 /* I18N: Name of a module/chart */ 111 return I18N::translate('Pedigree'); 112 } 113 114 /** 115 * A sentence describing what this module does. 116 * 117 * @return string 118 */ 119 public function description(): string 120 { 121 /* I18N: Description of the “PedigreeChart” module */ 122 return I18N::translate('A chart of an individual’s ancestors, formatted as a tree.'); 123 } 124 125 /** 126 * CSS class for the URL. 127 * 128 * @return string 129 */ 130 public function chartMenuClass(): string 131 { 132 return 'menu-chart-pedigree'; 133 } 134 135 /** 136 * Return a menu item for this chart - for use in individual boxes. 137 * 138 * @param Individual $individual 139 * 140 * @return Menu|null 141 */ 142 public function chartBoxMenu(Individual $individual): ?Menu 143 { 144 return $this->chartMenu($individual); 145 } 146 147 /** 148 * The title for a specific instance of this chart. 149 * 150 * @param Individual $individual 151 * 152 * @return string 153 */ 154 public function chartTitle(Individual $individual): string 155 { 156 /* I18N: %s is an individual’s name */ 157 return I18N::translate('Pedigree tree of %s', $individual->fullName()); 158 } 159 160 /** 161 * The URL for a page showing chart options. 162 * 163 * @param Individual $individual 164 * @param string[] $parameters 165 * 166 * @return string 167 */ 168 public function chartUrl(Individual $individual, array $parameters = []): string 169 { 170 return route(self::ROUTE_NAME, [ 171 'xref' => $individual->xref(), 172 'tree' => $individual->tree()->name(), 173 ] + $parameters + self::DEFAULT_PARAMETERS); 174 } 175 176 /** 177 * @param ServerRequestInterface $request 178 * 179 * @return ResponseInterface 180 */ 181 public function handle(ServerRequestInterface $request): ResponseInterface 182 { 183 $ajax = $request->getQueryParams()['ajax'] ?? ''; 184 $generations = (int) $request->getAttribute('generations'); 185 $style = $request->getAttribute('style'); 186 $tree = $request->getAttribute('tree'); 187 $user = $request->getAttribute('user'); 188 $xref = $request->getAttribute('xref'); 189 $individual = Individual::getInstance($xref, $tree); 190 191 // Convert POST requests into GET requests for pretty URLs. 192 if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 193 return redirect(route(self::ROUTE_NAME, [ 194 'tree' => $request->getAttribute('tree')->name(), 195 'xref' => $request->getParsedBody()['xref'], 196 'style' => $request->getParsedBody()['style'], 197 'generations' => $request->getParsedBody()['generations'], 198 ])); 199 } 200 201 Auth::checkIndividualAccess($individual); 202 Auth::checkComponentAccess($this, 'chart', $tree, $user); 203 204 $generations = min($generations, self::MAXIMUM_GENERATIONS); 205 $generations = max($generations, self::MINIMUM_GENERATIONS); 206 207 if ($ajax === '1') { 208 $this->layout = 'layouts/ajax'; 209 210 $ancestors = $this->chart_service->sosaStradonitzAncestors($individual, $generations); 211 212 // Father’s ancestors link to the father’s pedigree 213 // Mother’s ancestors link to the mother’s pedigree.. 214 $links = $ancestors->map(function (?Individual $individual, $sosa) use ($ancestors, $style, $generations): string { 215 if ($individual instanceof Individual && $sosa >= 2 ** $generations / 2 && $individual->childFamilies()->isNotEmpty()) { 216 // The last row/column, and there are more generations. 217 if ($sosa >= 2 ** $generations * 3 / 4) { 218 return $this->nextLink($ancestors->get(3), $style, $generations); 219 } 220 221 return $this->nextLink($ancestors->get(2), $style, $generations); 222 } 223 224 // A spacer to fix the "Left" layout. 225 return '<span class="invisible px-2">' . view('icons/arrow-' . $style) . '</span>'; 226 }); 227 228 // Root individual links to their children. 229 $links->put(1, $this->previousLink($individual, $style, $generations)); 230 231 return $this->viewResponse('modules/pedigree-chart/chart', [ 232 'ancestors' => $ancestors, 233 'generations' => $generations, 234 'style' => $style, 235 'layout' => 'right', 236 'links' => $links, 237 ]); 238 } 239 240 $ajax_url = $this->chartUrl($individual, [ 241 'ajax' => true, 242 'generations' => $generations, 243 'style' => $style, 244 'xref' => $xref, 245 ]); 246 247 return $this->viewResponse('modules/pedigree-chart/page', [ 248 'ajax_url' => $ajax_url, 249 'generations' => $generations, 250 'generation_options' => $this->generationOptions(), 251 'individual' => $individual, 252 'module' => $this->name(), 253 'style' => $style, 254 'styles' => $this->styles(), 255 'title' => $this->chartTitle($individual), 256 ]); 257 } 258 259 /** 260 * Build a menu for the chart root individual 261 * 262 * @param Individual $individual 263 * @param string $style 264 * @param int $generations 265 * 266 * @return string 267 */ 268 public function nextLink(Individual $individual, string $style, int $generations): string 269 { 270 $icon = view('icons/arrow-' . $style); 271 $title = $this->chartTitle($individual); 272 $url = $this->chartUrl($individual, [ 273 'style' => $style, 274 'generations' => $generations, 275 ]); 276 277 return '<a class="px-2" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $icon . '<span class="sr-only">' . $title . '</span></a>'; 278 } 279 280 /** 281 * Build a menu for the chart root individual 282 * 283 * @param Individual $individual 284 * @param string $style 285 * @param int $generations 286 * 287 * @return string 288 */ 289 public function previousLink(Individual $individual, string $style, int $generations): string 290 { 291 $icon = view('icons/arrow-' . self::MIRROR_STYLE[$style]); 292 293 $siblings = []; 294 $spouses = []; 295 $children = []; 296 297 foreach ($individual->childFamilies() as $family) { 298 foreach ($family->children() as $child) { 299 if ($child !== $individual) { 300 $siblings[] = $this->individualLink($child, $style, $generations); 301 } 302 } 303 } 304 305 foreach ($individual->spouseFamilies() as $family) { 306 foreach ($family->spouses() as $spouse) { 307 if ($spouse !== $individual) { 308 $spouses[] = $this->individualLink($spouse, $style, $generations); 309 } 310 } 311 312 foreach ($family->children() as $child) { 313 $children[] = $this->individualLink($child, $style, $generations); 314 } 315 } 316 317 return view('modules/pedigree-chart/previous', [ 318 'icon' => $icon, 319 'individual' => $individual, 320 'generations' => $generations, 321 'style' => $style, 322 'chart' => $this, 323 'siblings' => $siblings, 324 'spouses' => $spouses, 325 'children' => $children, 326 ]); 327 } 328 329 /** 330 * @param Individual $individual 331 * @param string $style 332 * @param int $generations 333 * 334 * @return string 335 */ 336 protected function individualLink(Individual $individual, string $style, int $generations): string 337 { 338 $text = $individual->fullName(); 339 $title = $this->chartTitle($individual); 340 $url = $this->chartUrl($individual, [ 341 'style' => $style, 342 'generations' => $generations, 343 ]); 344 345 return '<a class="dropdown-item" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $text . '</a>'; 346 } 347 348 /** 349 * @return string[] 350 */ 351 protected function generationOptions(): array 352 { 353 return FunctionsEdit::numericOptions(range(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)); 354 } 355 356 /** 357 * This chart can display its output in a number of styles 358 * 359 * @return string[] 360 */ 361 protected function styles(): array 362 { 363 return [ 364 self::STYLE_LEFT => I18N::translate('Left'), 365 self::STYLE_RIGHT => I18N::translate('Right'), 366 self::STYLE_UP => I18N::translate('Up'), 367 self::STYLE_DOWN => I18N::translate('Down'), 368 ]; 369 } 370} 371