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