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\Theme; 27use Fisharebest\Webtrees\Tree; 28use Fisharebest\Webtrees\User; 29use Symfony\Component\HttpFoundation\Request; 30use Symfony\Component\HttpFoundation\Response; 31 32/** 33 * Class HourglassChartModule 34 */ 35class HourglassChartModule extends AbstractModule implements ModuleChartInterface 36{ 37 use ModuleChartTrait; 38 39 // Defaults 40 private const DEFAULT_GENERATIONS = '3'; 41 private const DEFAULT_MAXIMUM_GENERATIONS = '9'; 42 43 // Limits 44 private const MINIMUM_GENERATIONS = 2; 45 46 /** 47 * How should this module be labelled on tabs, menus, etc.? 48 * 49 * @return string 50 */ 51 public function title(): string 52 { 53 /* I18N: Name of a module/chart */ 54 return I18N::translate('Hourglass chart'); 55 } 56 57 /** 58 * A sentence describing what this module does. 59 * 60 * @return string 61 */ 62 public function description(): string 63 { 64 /* I18N: Description of the “HourglassChart” module */ 65 return I18N::translate('An hourglass chart of an individual’s ancestors and descendants.'); 66 } 67 68 /** 69 * CSS class for the URL. 70 * 71 * @return string 72 */ 73 public function chartMenuClass(): string 74 { 75 return 'menu-chart-hourglass'; 76 } 77 78 /** 79 * Return a menu item for this chart - for use in individual boxes. 80 * 81 * @param Individual $individual 82 * 83 * @return Menu|null 84 */ 85 public function chartBoxMenu(Individual $individual): ?Menu 86 { 87 return $this->chartMenu($individual); 88 } 89 90 /** 91 * A form to request the chart parameters. 92 * 93 * @param Request $request 94 * @param Tree $tree 95 * @param User $user 96 * 97 * @return Response 98 */ 99 public function getChartAction(Request $request, Tree $tree, User $user): Response 100 { 101 $ajax = (bool) $request->get('ajax'); 102 $xref = $request->get('xref', ''); 103 $individual = Individual::getInstance($xref, $tree); 104 105 Auth::checkIndividualAccess($individual); 106 Auth::checkComponentAccess($this, 'chart', $tree, $user); 107 108 $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 109 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 110 111 $generations = (int) $request->get('generations', $default_generations); 112 113 $generations = min($generations, $maximum_generations); 114 $generations = max($generations, self::MINIMUM_GENERATIONS); 115 116 $show_spouse = (bool) $request->get('show_spouse'); 117 118 if ($ajax) { 119 return $this->chart($individual, $generations, $show_spouse); 120 } 121 122 $ajax_url = $this->chartUrl($individual, [ 123 'ajax' => true, 124 ]); 125 126 return $this->viewResponse('modules/hourglass-chart/page', [ 127 'ajax_url' => $ajax_url, 128 'generations' => $generations, 129 'individual' => $individual, 130 'maximum_generations' => $maximum_generations, 131 'minimum_generations' => self::MINIMUM_GENERATIONS, 132 'module_name' => $this->name(), 133 'show_spouse' => $show_spouse, 134 'title' => $this->chartTitle($individual), 135 ]); 136 } 137 138 /** 139 * Generate the initial generations of the chart 140 * 141 * @param Request $request 142 * @param Tree $tree 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) (Theme::theme()->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="' . Theme::theme()->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="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>'; 271 } else { 272 // Middle child 273 echo '<td style="background: url(\'' . Theme::theme()->parameter('image-vline') . '\');"><img alt="" role="presentation" src="' . Theme::theme()->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="', Theme::theme()->parameter('chart-box-x'), '">'; 282 } 283 284 // Print the descendency expansion arrow 285 if ($generation == $generations) { 286 $tbwidth = Theme::theme()->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:", Theme::theme()->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:", Theme::theme()->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:", Theme::theme()->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="' . Theme::theme()->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:", (Theme::theme()->parameter('chart-box-y') / 4), 'px; width:', Theme::theme()->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:', Theme::theme()->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> ' . Theme::theme()->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> ' . Theme::theme()->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="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>'; 450 echo '<td> <img alt="" role="presentation" class="lineh2" src="' . Theme::theme()->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 Theme::theme()->individualBoxEmpty(); 477 echo '</tr>'; 478 echo Theme::theme()->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="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>', 487 '<td> <img alt="" role="presentation" class="lineh3" src="' . Theme::theme()->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