1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Module; 19 20use Fisharebest\Webtrees\Auth; 21use Fisharebest\Webtrees\FontAwesome; 22use Fisharebest\Webtrees\Functions\FunctionsPrint; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Individual; 25use Fisharebest\Webtrees\Menu; 26use Fisharebest\Webtrees\Tree; 27use Fisharebest\Webtrees\User; 28use Symfony\Component\HttpFoundation\Request; 29use Symfony\Component\HttpFoundation\Response; 30 31/** 32 * Class HourglassChartModule 33 */ 34class HourglassChartModule extends AbstractModule implements ModuleChartInterface 35{ 36 use ModuleChartTrait; 37 38 // Defaults 39 private const DEFAULT_GENERATIONS = '3'; 40 private const DEFAULT_MAXIMUM_GENERATIONS = '9'; 41 42 // Limits 43 private const MINIMUM_GENERATIONS = 2; 44 45 /** 46 * How should this module be labelled on tabs, menus, etc.? 47 * 48 * @return string 49 */ 50 public function title(): string 51 { 52 /* I18N: Name of a module/chart */ 53 return I18N::translate('Hourglass chart'); 54 } 55 56 /** 57 * A sentence describing what this module does. 58 * 59 * @return string 60 */ 61 public function description(): string 62 { 63 /* I18N: Description of the “HourglassChart” module */ 64 return I18N::translate('An hourglass chart of an individual’s ancestors and descendants.'); 65 } 66 67 /** 68 * CSS class for the URL. 69 * 70 * @return string 71 */ 72 public function chartMenuClass(): string 73 { 74 return 'menu-chart-hourglass'; 75 } 76 77 /** 78 * Return a menu item for this chart - for use in individual boxes. 79 * 80 * @param Individual $individual 81 * 82 * @return Menu|null 83 */ 84 public function chartBoxMenu(Individual $individual): ?Menu 85 { 86 return $this->chartMenu($individual); 87 } 88 89 /** 90 * A form to request the chart parameters. 91 * 92 * @param Request $request 93 * @param Tree $tree 94 * @param User $user 95 * 96 * @return Response 97 */ 98 public function getChartAction(Request $request, Tree $tree, User $user): Response 99 { 100 $ajax = (bool) $request->get('ajax'); 101 $xref = $request->get('xref', ''); 102 $individual = Individual::getInstance($xref, $tree); 103 104 Auth::checkIndividualAccess($individual); 105 Auth::checkComponentAccess($this, 'chart', $tree, $user); 106 107 $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 108 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 109 110 $generations = (int) $request->get('generations', $default_generations); 111 112 $generations = min($generations, $maximum_generations); 113 $generations = max($generations, self::MINIMUM_GENERATIONS); 114 115 $show_spouse = (bool) $request->get('show_spouse'); 116 117 if ($ajax) { 118 return $this->chart($individual, $generations, $show_spouse); 119 } 120 121 $ajax_url = $this->chartUrl($individual, [ 122 'ajax' => true, 123 ]); 124 125 return $this->viewResponse('modules/hourglass-chart/page', [ 126 'ajax_url' => $ajax_url, 127 'generations' => $generations, 128 'individual' => $individual, 129 'maximum_generations' => $maximum_generations, 130 'minimum_generations' => self::MINIMUM_GENERATIONS, 131 'module_name' => $this->name(), 132 'show_spouse' => $show_spouse, 133 'title' => $this->chartTitle($individual), 134 ]); 135 } 136 137 /** 138 * Generate the initial generations of the chart 139 * 140 * @param Request $request 141 * @param Tree $tree 142 * 143 * @return Response 144 */ 145 protected function chart(Individual $individual, int $generations, bool $show_spouse): Response 146 { 147 ob_start(); 148 $this->printDescendency($individual, 1, $generations, $show_spouse, true); 149 $descendants = ob_get_clean(); 150 151 ob_start(); 152 $this->printPersonPedigree($individual, 1, $generations, $show_spouse); 153 $ancestors = ob_get_clean(); 154 155 return new Response(view('modules/hourglass-chart/chart', [ 156 'descendants' => $descendants, 157 'ancestors' => $ancestors, 158 'bhalfheight' => (int) (app()->make(ModuleThemeInterface::class)->parameter('chart-box-y') / 2), 159 'module_name' => $this->name(), 160 ])); 161 } 162 163 /** 164 * @param Request $request 165 * @param Tree $tree 166 * 167 * @return Response 168 */ 169 public function postAncestorsAction(Request $request, Tree $tree): Response 170 { 171 $xref = $request->get('xref', ''); 172 $individual = Individual::getInstance($xref, $tree); 173 174 Auth::checkIndividualAccess($individual); 175 176 $show_spouse = (bool) $request->get('show_spouse'); 177 178 ob_start(); 179 $this->printPersonPedigree($individual, 0, 1, $show_spouse); 180 $html = ob_get_clean(); 181 182 return new Response($html); 183 } 184 185 /** 186 * @param Request $request 187 * @param Tree $tree 188 * 189 * @return Response 190 */ 191 public function postDescendantsAction(Request $request, Tree $tree): Response 192 { 193 $xref = $request->get('xref', ''); 194 $individual = Individual::getInstance($xref, $tree); 195 196 $show_spouse = (bool) $request->get('show_spouse'); 197 198 ob_start(); 199 $this->printDescendency($individual, 1, 2, $show_spouse, false); 200 $html = ob_get_clean(); 201 202 return new Response($html); 203 } 204 205 /** 206 * Prints descendency of passed in person 207 * 208 * @param Individual $individual Show descendants of this individual 209 * @param int $generation The current generation number 210 * @param int $generations Show this number of generations 211 * @param bool $show_spouse 212 * @param bool $show_menu 213 * 214 * @return void 215 */ 216 private function printDescendency(Individual $individual, int $generation, int $generations, bool $show_spouse, bool $show_menu) 217 { 218 static $lastGenSecondFam = false; 219 220 if ($generation > $generations) { 221 return; 222 } 223 $pid = $individual->xref(); 224 $tablealign = 'right'; 225 $otablealign = 'left'; 226 if (I18N::direction() === 'rtl') { 227 $tablealign = 'left'; 228 $otablealign = 'right'; 229 } 230 231 //-- put a space between families on the last generation 232 if ($generation == $generations - 1) { 233 if ($lastGenSecondFam) { 234 echo '<br>'; 235 } 236 $lastGenSecondFam = true; 237 } 238 echo '<table cellspacing="0" cellpadding="0" border="0" id="table_' . e($pid) . '" class="hourglassChart" style="float:' . $tablealign . '">'; 239 echo '<tr>'; 240 echo '<td style="text-align:' . $tablealign . '">'; 241 $families = $individual->getSpouseFamilies(); 242 $children = []; 243 if ($generation < $generations) { 244 // Put all of the children in a common array 245 foreach ($families as $family) { 246 foreach ($family->getChildren() as $child) { 247 $children[] = $child; 248 } 249 } 250 251 $ct = count($children); 252 if ($ct > 0) { 253 echo '<table cellspacing="0" cellpadding="0" border="0" style="position: relative; top: auto; float: ' . $tablealign . ';">'; 254 for ($i = 0; $i < $ct; $i++) { 255 $individual2 = $children[$i]; 256 $chil = $individual2->xref(); 257 echo '<tr>'; 258 echo '<td id="td_', e($chil), '" class="', I18N::direction(), '" style="text-align:', $otablealign, '">'; 259 $this->printDescendency($individual2, $generation + 1, $generations, $show_spouse, false); 260 echo '</td>'; 261 262 // Print the lines 263 if ($ct > 1) { 264 if ($i == 0) { 265 // First child 266 echo '<td style="vertical-align:bottom"><img alt="" role="presentation" class="line1 tvertline" id="vline_' . $chil . '" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>'; 267 } elseif ($i == $ct - 1) { 268 // Last child 269 echo '<td style="vertical-align:top"><img alt="" role="presentation" class="bvertline" id="vline_' . $chil . '" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>'; 270 } else { 271 // Middle child 272 echo '<td style="background: url(\'' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '\');"><img alt="" role="presentation" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-spacer') . '" width="3"></td>'; 273 } 274 } 275 echo '</tr>'; 276 } 277 echo '</table>'; 278 } 279 echo '</td>'; 280 echo '<td class="myCharts" width="', app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), '">'; 281 } 282 283 // Print the descendency expansion arrow 284 if ($generation == $generations) { 285 $tbwidth = app()->make(ModuleThemeInterface::class)->parameter('chart-box-x') + 16; 286 for ($j = $generation; $j < $generations; $j++) { 287 echo "<div style='width: ", $tbwidth, "px;'><br></div></td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 288 } 289 $kcount = 0; 290 foreach ($families as $family) { 291 $kcount += $family->getNumberOfChildren(); 292 } 293 if ($kcount == 0) { 294 echo "</td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 295 } else { 296 echo FontAwesome::linkIcon('arrow-start', I18N::translate('Children'), [ 297 'href' => '#', 298 'data-route' => 'Descendants', 299 'data-xref' => $pid, 300 'data-spouses' => $show_spouse, 301 'data-tree' => $individual->tree()->name(), 302 ]); 303 304 //-- move the arrow up to line up with the correct box 305 if ($show_spouse) { 306 echo str_repeat('<br><br><br>', count($families)); 307 } 308 echo "</td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 309 } 310 } 311 312 echo '<table cellspacing="0" cellpadding="0" border="0" id="table2_' . $pid . '"><tr><td> '; 313 echo FunctionsPrint::printPedigreePerson($individual); 314 echo '</td><td> <img alt="" role="presentation" class="lineh1" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3">'; 315 316 //----- Print the spouse 317 if ($show_spouse) { 318 foreach ($families as $family) { 319 echo "</td></tr><tr><td style='text-align:$otablealign'>"; 320 echo FunctionsPrint::printPedigreePerson($family->getSpouse($individual)); 321 echo '</td><td> </td>'; 322 } 323 //-- add offset divs to make things line up better 324 if ($generation == $generations) { 325 echo "<tr><td colspan '2'><div style='height:", (app()->make(ModuleThemeInterface::class)->parameter('chart-box-y') / 4), 'px; width:', app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px;'><br></div>"; 326 } 327 } 328 echo '</td></tr></table>'; 329 330 // For the root individual, print a down arrow that allows changing the root of tree 331 if ($show_menu && $generation == 1) { 332 echo '<div class="center" id="childarrow" style="position:absolute; width:', app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), 'px;">'; 333 echo FontAwesome::linkIcon('arrow-down', I18N::translate('Family'), [ 334 'href' => '#', 335 'id' => 'spouse-child-links', 336 ]); 337 echo '<div id="childbox">'; 338 echo '<table cellspacing="0" cellpadding="0" border="0" class="person_box"><tr><td> '; 339 340 foreach ($individual->getSpouseFamilies() as $family) { 341 echo "<span class='name1'>" . I18N::translate('Family') . '</span>'; 342 $spouse = $family->getSpouse($individual); 343 if ($spouse !== null) { 344 echo '<a href="' . e(route('hourglass', [ 345 'xref' => $spouse->xref(), 346 'generations' => $generations, 347 'show_spouse' => (int) $show_spouse, 348 'ged' => $spouse->tree()->name(), 349 ])) . '" class="name1">' . $spouse->getFullName() . '</a>'; 350 } 351 foreach ($family->getChildren() as $child) { 352 echo '<a href="' . e(route('hourglass', [ 353 'xref' => $child->xref(), 354 'generations' => $generations, 355 'show_spouse' => (int) $show_spouse, 356 'ged' => $child->tree()->name(), 357 ])) . '" class="name1">' . $child->getFullName() . '</a>'; 358 } 359 } 360 361 //-- print the siblings 362 foreach ($individual->getChildFamilies() as $family) { 363 if ($family->getHusband() || $family->getWife()) { 364 echo "<span class='name1'>" . I18N::translate('Parents') . '</span>'; 365 $husb = $family->getHusband(); 366 if ($husb) { 367 echo '<a href="' . e(route('hourglass', [ 368 'xref' => $husb->xref(), 369 'generations' => $generations, 370 'show_spouse' => (int) $show_spouse, 371 'ged' => $husb->tree()->name(), 372 ])) . '" class="name1">' . $husb->getFullName() . '</a>'; 373 } 374 $wife = $family->getWife(); 375 if ($wife) { 376 echo '<a href="' . e(route('hourglass', [ 377 'xref' => $wife->xref(), 378 'generations' => $generations, 379 'show_spouse' => (int) $show_spouse, 380 'ged' => $wife->tree()->name(), 381 ])) . '" class="name1">' . $wife->getFullName() . '</a>'; 382 } 383 } 384 385 // filter out root person from children array so only siblings remain 386 $siblings = array_filter($family->getChildren(), function (Individual $x) use ($individual): bool { 387 return $x !== $individual; 388 }); 389 $count_siblings = count($siblings); 390 if ($count_siblings > 0) { 391 echo '<span class="name1">'; 392 echo $count_siblings > 1 ? I18N::translate('Siblings') : I18N::translate('Sibling'); 393 echo '</span>'; 394 foreach ($siblings as $child) { 395 echo '<a href="' . e(route('hourglass', [ 396 'xref' => $child->xref(), 397 'generations' => $generations, 398 'show_spouse' => (int) $show_spouse, 399 'ged' => $child->tree()->name(), 400 ])) . '" class="name1">' . $child->getFullName() . '</a>'; 401 } 402 } 403 } 404 echo '</td></tr></table>'; 405 echo '</div>'; 406 echo '</div>'; 407 } 408 echo '</td></tr></table>'; 409 } 410 411 /** 412 * Prints pedigree of the person passed in. Which is the descendancy 413 * 414 * @param Individual $individual Show the pedigree of this individual 415 * @param int $generation Current generation number 416 * @param int $generations Show this number of generations 417 * @param bool $show_spouse 418 * 419 * @return void 420 */ 421 private function printPersonPedigree(Individual $individual, int $generation, int $generations, bool $show_spouse) 422 { 423 if ($generation >= $generations) { 424 return; 425 } 426 427 // handle pedigree n generations lines 428 $genoffset = $generations; 429 430 $family = $individual->getPrimaryChildFamily(); 431 432 if ($family === null) { 433 // Prints empty table columns for children w/o parents up to the max generation 434 // This allows vertical line spacing to be consistent 435 echo '<table><tr><td> ' . app()->make(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 436 echo '<td> '; 437 // Recursively get the father’s family 438 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 439 echo '</td></tr>'; 440 echo '<tr><td> ' . app()->make(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 441 echo '<td> '; 442 // Recursively get the mother’s family 443 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 444 echo '</td><td> </tr></table>'; 445 } else { 446 echo '<table cellspacing="0" cellpadding="0" border="0" class="hourglassChart">'; 447 echo '<tr>'; 448 echo '<td style="vertical-align:bottom"><img alt="" role="presnentation" class="line3 pvline" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>'; 449 echo '<td> <img alt="" role="presentation" class="lineh2" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3"></td>'; 450 echo '<td class="myCharts"> '; 451 //-- print the father box 452 echo FunctionsPrint::printPedigreePerson($family->getHusband()); 453 echo '</td>'; 454 if ($family->getHusband()) { 455 $ARID = $family->getHusband()->xref(); 456 echo '<td id="td_' . e($ARID) . '">'; 457 458 if ($generation == $generations - 1 && $family->getHusband()->getChildFamilies()) { 459 echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [ 460 'href' => '#', 461 'data-route' => 'Ancestors', 462 'data-xref' => $ARID, 463 'data-spouses' => (int) $show_spouse, 464 'data-tree' => $family->getHusband()->tree()->name(), 465 ]); 466 } 467 468 $this->printPersonPedigree($family->getHusband(), $generation + 1, $generations, $show_spouse); 469 echo '</td>'; 470 } else { 471 echo '<td> '; 472 if ($generation < $genoffset - 1) { 473 echo '<table>'; 474 for ($i = $generation; $i < ((2 ** (($genoffset - 1) - $generation)) / 2) + 2; $i++) { 475 echo app()->make(ModuleThemeInterface::class)->individualBoxEmpty(); 476 echo '</tr>'; 477 echo app()->make(ModuleThemeInterface::class)->individualBoxEmpty(); 478 echo '</tr>'; 479 } 480 echo '</table>'; 481 } 482 } 483 echo 484 '</tr><tr>', 485 '<td style="vertical-align:top"><img alt="" role="presentation" class="pvline" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>', 486 '<td> <img alt="" role="presentation" class="lineh3" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3"></td>', 487 '<td class="myCharts"> '; 488 489 echo FunctionsPrint::printPedigreePerson($family->getWife()); 490 echo '</td>'; 491 if ($family->getWife()) { 492 $ARID = $family->getWife()->xref(); 493 echo '<td id="td_' . e($ARID) . '">'; 494 495 if ($generation == $generations - 1 && $family->getWife()->getChildFamilies()) { 496 echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [ 497 'href' => '#', 498 'data-route' => 'Ancestors', 499 'data-xref' => $ARID, 500 'data-spouses' => (int) $show_spouse, 501 'data-tree' => $family->getWife()->tree()->name(), 502 ]); 503 } 504 505 $this->printPersonPedigree($family->getWife(), $generation + 1, $generations, $show_spouse); 506 echo '</td>'; 507 } 508 echo '</tr></table>'; 509 } 510 } 511} 512