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