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