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