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\Functions\FunctionsPrint; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Individual; 25use Fisharebest\Webtrees\Menu; 26use Fisharebest\Webtrees\Tree; 27use Symfony\Component\HttpFoundation\Request; 28use Symfony\Component\HttpFoundation\Response; 29use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 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 MAXIMUM_GENERATIONS = 10; 44 private const MINIMUM_GENERATIONS = 2; 45 46 /** 47 * How should this module be identified in the control panel, 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 UserInterface $user 96 * 97 * @return Response 98 */ 99 public function getChartAction(Request $request, Tree $tree, UserInterface $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 $generations = (int) $request->get('generations', self::DEFAULT_GENERATIONS); 109 110 $generations = min($generations, self::MAXIMUM_GENERATIONS); 111 $generations = max($generations, self::MINIMUM_GENERATIONS); 112 113 $show_spouse = (bool) $request->get('show_spouse'); 114 115 if ($ajax) { 116 return $this->chart($individual, $generations, $show_spouse); 117 } 118 119 $ajax_url = $this->chartUrl($individual, [ 120 'ajax' => true, 121 'show_spouse' => $show_spouse, 122 ]); 123 124 return $this->viewResponse('modules/hourglass-chart/page', [ 125 'ajax_url' => $ajax_url, 126 'generations' => $generations, 127 'individual' => $individual, 128 'maximum_generations' => self::MAXIMUM_GENERATIONS, 129 'minimum_generations' => self::MINIMUM_GENERATIONS, 130 'module_name' => $this->name(), 131 'show_spouse' => $show_spouse, 132 'title' => $this->chartTitle($individual), 133 ]); 134 } 135 136 /** 137 * Generate the initial generations of the chart 138 * 139 * @param Individual $individual 140 * @param int $generations 141 * @param bool $show_spouse 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(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 $show_spouse = (bool) $request->get('show_spouse'); 194 $xref = $request->get('xref', ''); 195 $individual = Individual::getInstance($xref, $tree); 196 197 if ($individual === null) { 198 throw new NotFoundHttpException(); 199 } 200 201 ob_start(); 202 $this->printDescendency($individual, 1, 2, $show_spouse, false); 203 $html = ob_get_clean(); 204 205 return new Response($html); 206 } 207 208 /** 209 * Prints descendency of passed in person 210 * 211 * @param Individual $individual Show descendants of this individual 212 * @param int $generation The current generation number 213 * @param int $generations Show this number of generations 214 * @param bool $show_spouse 215 * @param bool $show_menu 216 * 217 * @return void 218 */ 219 private function printDescendency(Individual $individual, int $generation, int $generations, bool $show_spouse, bool $show_menu): void 220 { 221 static $lastGenSecondFam = false; 222 223 if ($generation > $generations) { 224 return; 225 } 226 $pid = $individual->xref(); 227 $tablealign = 'right'; 228 $otablealign = 'left'; 229 if (I18N::direction() === 'rtl') { 230 $tablealign = 'left'; 231 $otablealign = 'right'; 232 } 233 234 //-- put a space between families on the last generation 235 if ($generation == $generations - 1) { 236 if ($lastGenSecondFam) { 237 echo '<br>'; 238 } 239 $lastGenSecondFam = true; 240 } 241 echo '<table cellspacing="0" cellpadding="0" border="0" id="table_' . e($pid) . '" class="hourglassChart" style="float:' . $tablealign . '">'; 242 echo '<tr>'; 243 echo '<td style="text-align:' . $tablealign . '">'; 244 $families = $individual->spouseFamilies(); 245 $children = []; 246 if ($generation < $generations) { 247 // Put all of the children in a common array 248 foreach ($families as $family) { 249 foreach ($family->children() as $child) { 250 $children[] = $child; 251 } 252 } 253 254 $ct = count($children); 255 if ($ct > 0) { 256 echo '<table cellspacing="0" cellpadding="0" border="0" style="position: relative; top: auto; float: ' . $tablealign . ';">'; 257 for ($i = 0; $i < $ct; $i++) { 258 $individual2 = $children[$i]; 259 $chil = $individual2->xref(); 260 echo '<tr>'; 261 echo '<td id="td_', e($chil), '" class="', I18N::direction(), '" style="text-align:', $otablealign, '">'; 262 $this->printDescendency($individual2, $generation + 1, $generations, $show_spouse, false); 263 echo '</td>'; 264 265 // Print the lines 266 if ($ct > 1) { 267 if ($i == 0) { 268 // First child 269 echo '<td style="vertical-align:bottom"><img alt="" role="presentation" class="line1 tvertline" id="vline_' . $chil . '" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>'; 270 } elseif ($i == $ct - 1) { 271 // Last child 272 echo '<td style="vertical-align:top"><img alt="" role="presentation" class="bvertline" id="vline_' . $chil . '" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>'; 273 } else { 274 // Middle child 275 echo '<td style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ');"><img alt="" role="presentation" src="' . e(asset('css/images/spacer.png')) . '" width="3"></td>'; 276 } 277 } 278 echo '</tr>'; 279 } 280 echo '</table>'; 281 } 282 echo '</td>'; 283 echo '<td class="myCharts" width="', app(ModuleThemeInterface::class)->parameter('chart-box-x'), '">'; 284 } 285 286 // Print the descendency expansion arrow 287 if ($generation === $generations) { 288 $tbwidth = app(ModuleThemeInterface::class)->parameter('chart-box-x') + 16; 289 for ($j = $generation; $j < $generations; $j++) { 290 echo "<div style='width: ", $tbwidth, "px;'><br></div></td><td style='width:", app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 291 } 292 $kcount = 0; 293 foreach ($families as $family) { 294 $kcount += $family->numberOfChildren(); 295 } 296 if ($kcount == 0) { 297 echo "</td><td style='width:", app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 298 } else { 299 echo '<a href="#" title="' . I18N::translate('Children') . '" data-route="Descendants" data-xref="' . e($pid) . '" data-spouses="' . e($show_spouse) . '" data-tree="' . e($individual->tree()->name()) . '">' . view('icons/arrow-left') . '</a>'; 300 301 //-- move the arrow up to line up with the correct box 302 if ($show_spouse) { 303 echo str_repeat('<br><br><br>', count($families)); 304 } 305 echo "</td><td style='width:", app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>"; 306 } 307 } 308 309 echo '<table cellspacing="0" cellpadding="0" border="0" id="table2_' . $pid . '"><tr><td> '; 310 echo FunctionsPrint::printPedigreePerson($individual); 311 echo '</td><td> <img alt="" role="presentation" class="lineh1" src="' . e(asset('css/images/hline.png')) . '" width="7" height="3">'; 312 313 //----- Print the spouse 314 if ($show_spouse) { 315 foreach ($families as $family) { 316 echo "</td></tr><tr><td style='text-align:$otablealign'>"; 317 echo FunctionsPrint::printPedigreePerson($family->spouse($individual)); 318 echo '</td><td> </td>'; 319 } 320 //-- add offset divs to make things line up better 321 if ($generation == $generations) { 322 echo "<tr><td colspan '2'><div style='height:", (app(ModuleThemeInterface::class)->parameter('chart-box-y') / 4), 'px; width:', app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px;'><br></div>"; 323 } 324 } 325 echo '</td></tr></table>'; 326 327 // For the root individual, print a down arrow that allows changing the root of tree 328 if ($show_menu && $generation == 1) { 329 echo '<div class="text-center" id="childarrow" style="position:absolute; width:', app(ModuleThemeInterface::class)->parameter('chart-box-x'), 'px;">'; 330 echo '<a href="#" title="' . I18N::translate('Family') . '" id="spouse-child-links">' . view('icons/arrow-down') . '</a>'; 331 echo '<div id="childbox">'; 332 echo '<table cellspacing="0" cellpadding="0" border="0" class="person_box"><tr><td> '; 333 334 foreach ($individual->spouseFamilies() as $family) { 335 echo "<span class='name1'>" . I18N::translate('Family') . '</span>'; 336 $spouse = $family->spouse($individual); 337 if ($spouse !== null) { 338 echo '<a href="' . e(route('hourglass', [ 339 'xref' => $spouse->xref(), 340 'generations' => $generations, 341 'show_spouse' => (int) $show_spouse, 342 'ged' => $spouse->tree()->name(), 343 ])) . '" class="name1">' . $spouse->fullName() . '</a>'; 344 } 345 foreach ($family->children() as $child) { 346 echo '<a href="' . e(route('hourglass', [ 347 'xref' => $child->xref(), 348 'generations' => $generations, 349 'show_spouse' => (int) $show_spouse, 350 'ged' => $child->tree()->name(), 351 ])) . '" class="name1">' . $child->fullName() . '</a>'; 352 } 353 } 354 355 //-- print the siblings 356 foreach ($individual->childFamilies() as $family) { 357 if ($family->husband() || $family->wife()) { 358 echo "<span class='name1'>" . I18N::translate('Parents') . '</span>'; 359 $husb = $family->husband(); 360 if ($husb) { 361 echo '<a href="' . e(route('hourglass', [ 362 'xref' => $husb->xref(), 363 'generations' => $generations, 364 'show_spouse' => (int) $show_spouse, 365 'ged' => $husb->tree()->name(), 366 ])) . '" class="name1">' . $husb->fullName() . '</a>'; 367 } 368 $wife = $family->wife(); 369 if ($wife) { 370 echo '<a href="' . e(route('hourglass', [ 371 'xref' => $wife->xref(), 372 'generations' => $generations, 373 'show_spouse' => (int) $show_spouse, 374 'ged' => $wife->tree()->name(), 375 ])) . '" class="name1">' . $wife->fullName() . '</a>'; 376 } 377 } 378 379 // filter out root person from children array so only siblings remain 380 $siblings = $family->children()->filter(function (Individual $x) use ($individual): bool { 381 return $x !== $individual; 382 }); 383 384 if ($siblings->count() > 0) { 385 echo '<span class="name1">'; 386 echo $siblings->count() > 1 ? I18N::translate('Siblings') : I18N::translate('Sibling'); 387 echo '</span>'; 388 foreach ($siblings as $child) { 389 echo '<a href="' . e(route('hourglass', [ 390 'xref' => $child->xref(), 391 'generations' => $generations, 392 'show_spouse' => (int) $show_spouse, 393 'ged' => $child->tree()->name(), 394 ])) . '" class="name1">' . $child->fullName() . '</a>'; 395 } 396 } 397 } 398 echo '</td></tr></table>'; 399 echo '</div>'; 400 echo '</div>'; 401 } 402 echo '</td></tr></table>'; 403 } 404 405 /** 406 * Prints pedigree of the person passed in. Which is the descendancy 407 * 408 * @param Individual $individual Show the pedigree of this individual 409 * @param int $generation Current generation number 410 * @param int $generations Show this number of generations 411 * @param bool $show_spouse 412 * 413 * @return void 414 */ 415 private function printPersonPedigree(Individual $individual, int $generation, int $generations, bool $show_spouse): void 416 { 417 if ($generation >= $generations) { 418 return; 419 } 420 421 // handle pedigree n generations lines 422 $genoffset = $generations; 423 424 $family = $individual->primaryChildFamily(); 425 426 if ($family === null) { 427 // Prints empty table columns for children w/o parents up to the max generation 428 // This allows vertical line spacing to be consistent 429 echo '<table><tr><td> ' . app(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 430 echo '<td> '; 431 // Recursively get the father’s family 432 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 433 echo '</td></tr>'; 434 echo '<tr><td> ' . app(ModuleThemeInterface::class)->individualBoxEmpty() . '</td>'; 435 echo '<td> '; 436 // Recursively get the mother’s family 437 $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse); 438 echo '</td><td> </tr></table>'; 439 } else { 440 echo '<table cellspacing="0" cellpadding="0" border="0" class="hourglassChart">'; 441 echo '<tr>'; 442 echo '<td style="vertical-align:bottom"><img alt="" role="presnentation" class="line3 pvline" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>'; 443 echo '<td> <img alt="" role="presentation" class="lineh2" src="' . e(asset('css/images/hline.png')) . '" width="7" height="3"></td>'; 444 echo '<td class="myCharts"> '; 445 //-- print the father box 446 echo FunctionsPrint::printPedigreePerson($family->husband()); 447 echo '</td>'; 448 if ($family->husband()) { 449 $ARID = $family->husband()->xref(); 450 echo '<td id="td_' . e($ARID) . '">'; 451 452 if ($generation == $generations - 1 && $family->husband()->childFamilies()) { 453 echo '<a href="#" title="' . I18N::translate('Parents') . '" data-route="Ancestors" data-xref="' . e($ARID) . '" data-spouses="' . e($show_spouse) . '" data-tree="' . e($family->husband()->tree()->name()) . '">' . view('icons/arrow-right') . '</a>'; 454 } 455 456 $this->printPersonPedigree($family->husband(), $generation + 1, $generations, $show_spouse); 457 echo '</td>'; 458 } else { 459 echo '<td> '; 460 if ($generation < $genoffset - 1) { 461 echo '<table>'; 462 for ($i = $generation; $i < ((2 ** (($genoffset - 1) - $generation)) / 2) + 2; $i++) { 463 echo app(ModuleThemeInterface::class)->individualBoxEmpty(); 464 echo '</tr>'; 465 echo app(ModuleThemeInterface::class)->individualBoxEmpty(); 466 echo '</tr>'; 467 } 468 echo '</table>'; 469 } 470 } 471 echo 472 '</tr><tr>', 473 '<td style="vertical-align:top"><img alt="" role="presentation" class="pvline" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>', 474 '<td> <img alt="" role="presentation" class="lineh3" src="' . e(asset('css/images/hline.png')) . '" width="7" height="3"></td>', 475 '<td class="myCharts"> '; 476 477 echo FunctionsPrint::printPedigreePerson($family->wife()); 478 echo '</td>'; 479 if ($family->wife()) { 480 $ARID = $family->wife()->xref(); 481 echo '<td id="td_' . e($ARID) . '">'; 482 483 if ($generation == $generations - 1 && $family->wife()->childFamilies()) { 484 echo '<a href="#" title="' . I18N::translate('Parents') . '" data-route="Ancestors" data-xref="' . e($ARID) . '" data-spouses="' . e($show_spouse) . '" data-tree="' . e($family->wife()->tree()->name()) . '">' . view('icons/arrow-right') . '</a>'; 485 } 486 487 $this->printPersonPedigree($family->wife(), $generation + 1, $generations, $show_spouse); 488 echo '</td>'; 489 } 490 echo '</tr></table>'; 491 } 492 } 493} 494