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