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