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\Contracts\UserInterface; 22use Fisharebest\Webtrees\FontAwesome; 23use Fisharebest\Webtrees\Functions\FunctionsPrint; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Individual; 26use Fisharebest\Webtrees\Menu; 27use Fisharebest\Webtrees\Tree; 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 UserInterface $user 95 * 96 * @return Response 97 */ 98 public function getChartAction(Request $request, Tree $tree, UserInterface $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 Individual $individual 141 * @param int $generations 142 * @param bool $show_spouse 143 * 144 * @return Response 145 */ 146 protected function chart(Individual $individual, int $generations, bool $show_spouse): Response 147 { 148 ob_start(); 149 $this->printDescendency($individual, 1, $generations, $show_spouse, true); 150 $descendants = ob_get_clean(); 151 152 ob_start(); 153 $this->printPersonPedigree($individual, 1, $generations, $show_spouse); 154 $ancestors = ob_get_clean(); 155 156 return new Response(view('modules/hourglass-chart/chart', [ 157 'descendants' => $descendants, 158 'ancestors' => $ancestors, 159 'bhalfheight' => (int) (app()->make(ModuleThemeInterface::class)->parameter('chart-box-y') / 2), 160 'module_name' => $this->name(), 161 ])); 162 } 163 164 /** 165 * @param Request $request 166 * @param Tree $tree 167 * 168 * @return Response 169 */ 170 public function postAncestorsAction(Request $request, Tree $tree): Response 171 { 172 $xref = $request->get('xref', ''); 173 $individual = Individual::getInstance($xref, $tree); 174 175 Auth::checkIndividualAccess($individual); 176 177 $show_spouse = (bool) $request->get('show_spouse'); 178 179 ob_start(); 180 $this->printPersonPedigree($individual, 0, 1, $show_spouse); 181 $html = ob_get_clean(); 182 183 return new Response($html); 184 } 185 186 /** 187 * @param Request $request 188 * @param Tree $tree 189 * 190 * @return Response 191 */ 192 public function postDescendantsAction(Request $request, Tree $tree): Response 193 { 194 $xref = $request->get('xref', ''); 195 $individual = Individual::getInstance($xref, $tree); 196 197 $show_spouse = (bool) $request->get('show_spouse'); 198 199 ob_start(); 200 $this->printDescendency($individual, 1, 2, $show_spouse, false); 201 $html = ob_get_clean(); 202 203 return new Response($html); 204 } 205 206 /** 207 * Prints descendency of passed in person 208 * 209 * @param Individual $individual Show descendants of this individual 210 * @param int $generation The current generation number 211 * @param int $generations Show this number of generations 212 * @param bool $show_spouse 213 * @param bool $show_menu 214 * 215 * @return void 216 */ 217 private function printDescendency(Individual $individual, int $generation, int $generations, bool $show_spouse, bool $show_menu) 218 { 219 static $lastGenSecondFam = false; 220 221 if ($generation > $generations) { 222 return; 223 } 224 $pid = $individual->xref(); 225 $tablealign = 'right'; 226 $otablealign = 'left'; 227 if (I18N::direction() === 'rtl') { 228 $tablealign = 'left'; 229 $otablealign = 'right'; 230 } 231 232 //-- put a space between families on the last generation 233 if ($generation == $generations - 1) { 234 if ($lastGenSecondFam) { 235 echo '<br>'; 236 } 237 $lastGenSecondFam = true; 238 } 239 echo '<table cellspacing="0" cellpadding="0" border="0" id="table_' . e($pid) . '" class="hourglassChart" style="float:' . $tablealign . '">'; 240 echo '<tr>'; 241 echo '<td style="text-align:' . $tablealign . '">'; 242 $families = $individual->getSpouseFamilies(); 243 $children = []; 244 if ($generation < $generations) { 245 // Put all of the children in a common array 246 foreach ($families as $family) { 247 foreach ($family->getChildren() as $child) { 248 $children[] = $child; 249 } 250 } 251 252 $ct = count($children); 253 if ($ct > 0) { 254 echo '<table cellspacing="0" cellpadding="0" border="0" style="position: relative; top: auto; float: ' . $tablealign . ';">'; 255 for ($i = 0; $i < $ct; $i++) { 256 $individual2 = $children[$i]; 257 $chil = $individual2->xref(); 258 echo '<tr>'; 259 echo '<td id="td_', e($chil), '" class="', I18N::direction(), '" style="text-align:', $otablealign, '">'; 260 $this->printDescendency($individual2, $generation + 1, $generations, $show_spouse, false); 261 echo '</td>'; 262 263 // Print the lines 264 if ($ct > 1) { 265 if ($i == 0) { 266 // First child 267 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>'; 268 } elseif ($i == $ct - 1) { 269 // Last child 270 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>'; 271 } else { 272 // Middle child 273 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>'; 274 } 275 } 276 echo '</tr>'; 277 } 278 echo '</table>'; 279 } 280 echo '</td>'; 281 echo '<td class="myCharts" width="', app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), '">'; 282 } 283 284 // Print the descendency expansion arrow 285 if ($generation == $generations) { 286 $tbwidth = app()->make(ModuleThemeInterface::class)->parameter('chart-box-x') + 16; 287 for ($j = $generation; $j < $generations; $j++) { 288 echo "<div style='width: ", $tbwidth, "px;'><br></div></td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 289 } 290 $kcount = 0; 291 foreach ($families as $family) { 292 $kcount += $family->getNumberOfChildren(); 293 } 294 if ($kcount == 0) { 295 echo "</td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 296 } else { 297 echo FontAwesome::linkIcon('arrow-start', I18N::translate('Children'), [ 298 'href' => '#', 299 'data-route' => 'Descendants', 300 'data-xref' => $pid, 301 'data-spouses' => $show_spouse, 302 'data-tree' => $individual->tree()->name(), 303 ]); 304 305 //-- move the arrow up to line up with the correct box 306 if ($show_spouse) { 307 echo str_repeat('<br><br><br>', count($families)); 308 } 309 echo "</td><td style='width:", app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 310 } 311 } 312 313 echo '<table cellspacing="0" cellpadding="0" border="0" id="table2_' . $pid . '"><tr><td> '; 314 echo FunctionsPrint::printPedigreePerson($individual); 315 echo '</td><td> <img alt="" role="presentation" class="lineh1" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3">'; 316 317 //----- Print the spouse 318 if ($show_spouse) { 319 foreach ($families as $family) { 320 echo "</td></tr><tr><td style='text-align:$otablealign'>"; 321 echo FunctionsPrint::printPedigreePerson($family->getSpouse($individual)); 322 echo '</td><td> </td>'; 323 } 324 //-- add offset divs to make things line up better 325 if ($generation == $generations) { 326 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>"; 327 } 328 } 329 echo '</td></tr></table>'; 330 331 // For the root individual, print a down arrow that allows changing the root of tree 332 if ($show_menu && $generation == 1) { 333 echo '<div class="center" id="childarrow" style="position:absolute; width:', app()->make(ModuleThemeInterface::class)->parameter('chart-box-x'), 'px;">'; 334 echo FontAwesome::linkIcon('arrow-down', I18N::translate('Family'), [ 335 'href' => '#', 336 'id' => 'spouse-child-links', 337 ]); 338 echo '<div id="childbox">'; 339 echo '<table cellspacing="0" cellpadding="0" border="0" class="person_box"><tr><td> '; 340 341 foreach ($individual->getSpouseFamilies() as $family) { 342 echo "<span class='name1'>" . I18N::translate('Family') . '</span>'; 343 $spouse = $family->getSpouse($individual); 344 if ($spouse !== null) { 345 echo '<a href="' . e(route('hourglass', [ 346 'xref' => $spouse->xref(), 347 'generations' => $generations, 348 'show_spouse' => (int) $show_spouse, 349 'ged' => $spouse->tree()->name(), 350 ])) . '" class="name1">' . $spouse->getFullName() . '</a>'; 351 } 352 foreach ($family->getChildren() as $child) { 353 echo '<a href="' . e(route('hourglass', [ 354 'xref' => $child->xref(), 355 'generations' => $generations, 356 'show_spouse' => (int) $show_spouse, 357 'ged' => $child->tree()->name(), 358 ])) . '" class="name1">' . $child->getFullName() . '</a>'; 359 } 360 } 361 362 //-- print the siblings 363 foreach ($individual->getChildFamilies() as $family) { 364 if ($family->getHusband() || $family->getWife()) { 365 echo "<span class='name1'>" . I18N::translate('Parents') . '</span>'; 366 $husb = $family->getHusband(); 367 if ($husb) { 368 echo '<a href="' . e(route('hourglass', [ 369 'xref' => $husb->xref(), 370 'generations' => $generations, 371 'show_spouse' => (int) $show_spouse, 372 'ged' => $husb->tree()->name(), 373 ])) . '" class="name1">' . $husb->getFullName() . '</a>'; 374 } 375 $wife = $family->getWife(); 376 if ($wife) { 377 echo '<a href="' . e(route('hourglass', [ 378 'xref' => $wife->xref(), 379 'generations' => $generations, 380 'show_spouse' => (int) $show_spouse, 381 'ged' => $wife->tree()->name(), 382 ])) . '" class="name1">' . $wife->getFullName() . '</a>'; 383 } 384 } 385 386 // filter out root person from children array so only siblings remain 387 $siblings = array_filter($family->getChildren(), function (Individual $x) use ($individual): bool { 388 return $x !== $individual; 389 }); 390 $count_siblings = count($siblings); 391 if ($count_siblings > 0) { 392 echo '<span class="name1">'; 393 echo $count_siblings > 1 ? I18N::translate('Siblings') : I18N::translate('Sibling'); 394 echo '</span>'; 395 foreach ($siblings as $child) { 396 echo '<a href="' . e(route('hourglass', [ 397 'xref' => $child->xref(), 398 'generations' => $generations, 399 'show_spouse' => (int) $show_spouse, 400 'ged' => $child->tree()->name(), 401 ])) . '" class="name1">' . $child->getFullName() . '</a>'; 402 } 403 } 404 } 405 echo '</td></tr></table>'; 406 echo '</div>'; 407 echo '</div>'; 408 } 409 echo '</td></tr></table>'; 410 } 411 412 /** 413 * Prints pedigree of the person passed in. Which is the descendancy 414 * 415 * @param Individual $individual Show the pedigree of this individual 416 * @param int $generation Current generation number 417 * @param int $generations Show this number of generations 418 * @param bool $show_spouse 419 * 420 * @return void 421 */ 422 private function printPersonPedigree(Individual $individual, int $generation, int $generations, bool $show_spouse) 423 { 424 if ($generation >= $generations) { 425 return; 426 } 427 428 // handle pedigree n generations lines 429 $genoffset = $generations; 430 431 $family = $individual->getPrimaryChildFamily(); 432 433 if ($family === null) { 434 // Prints empty table columns for children w/o parents up to the max generation 435 // This allows vertical line spacing to be consistent 436 echo '<table><tr><td> ' . app()->make(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 437 echo '<td> '; 438 // Recursively get the father’s family 439 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 440 echo '</td></tr>'; 441 echo '<tr><td> ' . app()->make(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 442 echo '<td> '; 443 // Recursively get the mother’s family 444 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 445 echo '</td><td> </tr></table>'; 446 } else { 447 echo '<table cellspacing="0" cellpadding="0" border="0" class="hourglassChart">'; 448 echo '<tr>'; 449 echo '<td style="vertical-align:bottom"><img alt="" role="presnentation" class="line3 pvline" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>'; 450 echo '<td> <img alt="" role="presentation" class="lineh2" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3"></td>'; 451 echo '<td class="myCharts"> '; 452 //-- print the father box 453 echo FunctionsPrint::printPedigreePerson($family->getHusband()); 454 echo '</td>'; 455 if ($family->getHusband()) { 456 $ARID = $family->getHusband()->xref(); 457 echo '<td id="td_' . e($ARID) . '">'; 458 459 if ($generation == $generations - 1 && $family->getHusband()->getChildFamilies()) { 460 echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [ 461 'href' => '#', 462 'data-route' => 'Ancestors', 463 'data-xref' => $ARID, 464 'data-spouses' => (int) $show_spouse, 465 'data-tree' => $family->getHusband()->tree()->name(), 466 ]); 467 } 468 469 $this->printPersonPedigree($family->getHusband(), $generation + 1, $generations, $show_spouse); 470 echo '</td>'; 471 } else { 472 echo '<td> '; 473 if ($generation < $genoffset - 1) { 474 echo '<table>'; 475 for ($i = $generation; $i < ((2 ** (($genoffset - 1) - $generation)) / 2) + 2; $i++) { 476 echo app()->make(ModuleThemeInterface::class)->individualBoxEmpty(); 477 echo '</tr>'; 478 echo app()->make(ModuleThemeInterface::class)->individualBoxEmpty(); 479 echo '</tr>'; 480 } 481 echo '</table>'; 482 } 483 } 484 echo 485 '</tr><tr>', 486 '<td style="vertical-align:top"><img alt="" role="presentation" class="pvline" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-vline') . '" width="3"></td>', 487 '<td> <img alt="" role="presentation" class="lineh3" src="' . app()->make(ModuleThemeInterface::class)->parameter('image-hline') . '" width="7" height="3"></td>', 488 '<td class="myCharts"> '; 489 490 echo FunctionsPrint::printPedigreePerson($family->getWife()); 491 echo '</td>'; 492 if ($family->getWife()) { 493 $ARID = $family->getWife()->xref(); 494 echo '<td id="td_' . e($ARID) . '">'; 495 496 if ($generation == $generations - 1 && $family->getWife()->getChildFamilies()) { 497 echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [ 498 'href' => '#', 499 'data-route' => 'Ancestors', 500 'data-xref' => $ARID, 501 'data-spouses' => (int) $show_spouse, 502 'data-tree' => $family->getWife()->tree()->name(), 503 ]); 504 } 505 506 $this->printPersonPedigree($family->getWife(), $generation + 1, $generations, $show_spouse); 507 echo '</td>'; 508 } 509 echo '</tr></table>'; 510 } 511 } 512} 513