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