1<?php 2 3declare(strict_types=1); 4 5use Fisharebest\Webtrees\Age; 6use Fisharebest\Webtrees\Auth; 7use Fisharebest\Webtrees\Carbon; 8use Fisharebest\Webtrees\Date; 9use Fisharebest\Webtrees\GedcomTag; 10use Fisharebest\Webtrees\I18N; 11use Fisharebest\Webtrees\Individual; 12use Fisharebest\Webtrees\Module\ModuleChartInterface; 13use Fisharebest\Webtrees\Module\ModuleInterface; 14use Fisharebest\Webtrees\Module\RelationshipsChartModule; 15use Fisharebest\Webtrees\Registry; 16use Fisharebest\Webtrees\Services\ModuleService; 17use Fisharebest\Webtrees\Tree; 18use Fisharebest\Webtrees\View; 19use Illuminate\Support\Collection; 20use Ramsey\Uuid\Uuid; 21 22/** 23 * @var Collection<Individual> $individuals 24 * @var bool $sosa 25 * @var Tree $tree 26 */ 27 28// lists requires a unique ID in case there are multiple lists per page 29$table_id = 'table-indi-' . Uuid::uuid4()->toString(); 30 31$today_jd = Carbon::now()->julianDay(); 32$hundred_years_ago = Carbon::now()->subYears(100)->julianDay(); 33 34$unique_indis = []; // Don't double-count indis with multiple names. 35 36$show_estimated_dates = (bool) $tree->getPreference('SHOW_EST_LIST_DATES'); 37 38$today = new Date(strtoupper(date('d M Y'))); 39 40$module = app(ModuleService::class) 41 ->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 42 ->first(static function (ModuleInterface $module) { 43 return $module instanceof RelationshipsChartModule; 44 }); 45?> 46 47<?php View::push('javascript') ?> 48<script> 49$("#<?= e($table_id) ?> > .wt-table-individual").dataTable({ 50 processing: true, 51 retrieve: true, 52 columns: [ 53 /* Given names */ { type: "text" }, 54 /* Surnames */ { type: "text" }, 55 /* SOSA number */ { type: "num", visible: <?= json_encode($sosa) ?> }, 56 /* Birth date */ { type: "num" }, 57 /* Anniversary */ { type: "num" }, 58 /* Birthplace */ { type: "text" }, 59 /* Children */ { type: "num" }, 60 /* Deate date */ { type: "num" }, 61 /* Anniversary */ { type: "num" }, 62 /* Age */ { type: "num" }, 63 /* Death place */ { type: "text" }, 64 /* Last change */ { visible: <?= json_encode($tree->getPreference('SHOW_LAST_CHANGE')) ?> }, 65 /* Filter sex */ { sortable: false }, 66 /* Filter birth */ { sortable: false }, 67 /* Filter death */ { sortable: false }, 68 /* Filter tree */ { sortable: false } 69 ], 70 sorting: <?= json_encode($sosa ? [[4, 'asc']] : [[1, 'asc']]) ?> 71}); 72 73$("#<?= e($table_id) ?>") 74 /* Hide/show parents */ 75 .on("click", "#btn-toggle-parents", function() { 76 $(".wt-individual-list-parents").slideToggle(); 77 }) 78 /* Hide/show statistics */ 79 .on("click", "#btn-toggle-statistics", function() { 80 $("#individual-charts-<?= e($table_id) ?>").slideToggle({ 81 complete: function () { 82 // Trigger resize to redraw the chart 83 $('div[id^="google-chart-"]').resize(); 84 } 85 }); 86 }) 87 /* Filter buttons in table header */ 88 .on("click", "input[data-filter-column]", function() { 89 let checkbox = $(this); 90 let siblings = checkbox.parent().siblings(); 91 92 // Deselect other options 93 siblings.children().prop("checked", false).removeAttr("checked"); 94 siblings.removeClass('active'); 95 96 // Apply (or clear) this filter 97 let checked = checkbox.prop("checked"); 98 let filter = checked ? checkbox.data("filter-value") : ""; 99 let column = $("#<?= e($table_id) ?> .wt-table-individual").DataTable().column(checkbox.data("filter-column")); 100 column.search(filter).draw(); 101 }); 102</script> 103<?php View::endpush() ?> 104 105<?php 106$max_age = (int) $tree->getPreference('MAX_ALIVE_AGE'); 107 108// Initialise chart data 109$deat_by_age = []; 110for ($age = 0; $age <= $max_age; $age++) { 111 $deat_by_age[$age]['M'] = 0; 112 $deat_by_age[$age]['F'] = 0; 113 $deat_by_age[$age]['U'] = 0; 114} 115$birt_by_decade = []; 116$deat_by_decade = []; 117for ($year = 1400; $year < 2050; $year += 10) { 118 $birt_by_decade[$year]['M'] = 0; 119 $birt_by_decade[$year]['F'] = 0; 120 $birt_by_decade[$year]['U'] = 0; 121 $deat_by_decade[$year]['M'] = 0; 122 $deat_by_decade[$year]['F'] = 0; 123 $deat_by_decade[$year]['U'] = 0; 124} 125 126$birthData = [ 127 [ 128 [ 129 'label' => I18N::translate('Century'), 130 'type' => 'date', 131 ], [ 132 'label' => I18N::translate('Males'), 133 'type' => 'number', 134 ], [ 135 'label' => I18N::translate('Females'), 136 'type' => 'number', 137 ], 138 ] 139]; 140 141$deathData = [ 142 [ 143 [ 144 'label' => I18N::translate('Century'), 145 'type' => 'date', 146 ], [ 147 'label' => I18N::translate('Males'), 148 'type' => 'number', 149 ], [ 150 'label' => I18N::translate('Females'), 151 'type' => 'number', 152 ], 153 ] 154]; 155 156$deathAgeData = [ 157 [ 158 I18N::translate('Age'), 159 I18N::translate('Males'), 160 I18N::translate('Females'), 161 I18N::translate('Average age'), 162 ] 163]; 164 165?> 166 167<div id="<?= e($table_id) ?>"> 168 <table class="table table-bordered table-sm wt-table-individual" 169 <?= view('lists/datatables-attributes') ?> 170 > 171 <thead> 172 <tr> 173 <th colspan="16"> 174 <div class="btn-toolbar d-flex justify-content-between mb-2" role="toolbar"> 175 <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons"> 176 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show only males.') ?>"> 177 <input type="checkbox" data-filter-column="12" data-filter-value="M" autocomplete="off"> 178 <?= view('icons/sex', ['sex' => 'M']) ?> 179 </label> 180 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show only females.') ?>"> 181 <input type="checkbox" data-filter-column="12" data-filter-value="F" autocomplete="off"> 182 <?= view('icons/sex', ['sex' => 'F']) ?> 183 </label> 184 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show only individuals for whom the gender is not known.') ?>"> 185 <input type="checkbox" data-filter-column="12" data-filter-value="U" autocomplete="off"> 186 <?= view('icons/sex', ['sex' => 'U']) ?> 187 </label> 188 </div> 189 190 <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons"> 191 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals who are alive or couples where both partners are alive.') ?>"> 192 <input type="checkbox" data-filter-column="14" data-filter-value="N" autocomplete="off"> 193 <?= I18N::translate('Alive') ?> 194 </label> 195 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals who are dead or couples where both partners are dead.') ?>"> 196 <input type="checkbox" data-filter-column="14" data-filter-value="Y" autocomplete="off"> 197 <?= I18N::translate('Dead') ?> 198 </label> 199 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals who died more than 100 years ago.') ?>"> 200 <input type="checkbox" data-filter-column="14" data-filter-value="YES" autocomplete="off"> 201 <?= I18N::translate('Death') ?>>100 202 </label> 203 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals who died within the last 100 years.') ?>"> 204 <input type="checkbox" data-filter-column="14" data-filter-value="Y100" autocomplete="off"> 205 <?= I18N::translate('Death') ?><=100 206 </label> 207 </div> 208 209 <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons"> 210 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals born more than 100 years ago.') ?>"> 211 <input type="checkbox" data-filter-column="13" data-filter-value="YES" autocomplete="off"> 212 <?= I18N::translate('Birth') ?>>100 213 </label> 214 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals born within the last 100 years.') ?>"> 215 <input type="checkbox" data-filter-column="13" data-filter-value="Y100" autocomplete="off"> 216 <?= I18N::translate('Birth') ?><=100 217 </label> 218 </div> 219 220 <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons"> 221 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') ?>"> 222 <input type="checkbox" data-filter-column="15" data-filter-value="R" autocomplete="off"> 223 <?= I18N::translate('Roots') ?> 224 </label> 225 <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') ?>"> 226 <input type="checkbox" data-filter-column="15" data-filter-value="L" autocomplete="off"> 227 <?= I18N::translate('Leaves') ?> 228 </label> 229 </div> 230 </div> 231 </th> 232 </tr> 233 <tr> 234 <th><?= I18N::translate('Given names') ?></th> 235 <th><?= I18N::translate('Surname') ?></th> 236 <th><?= /* I18N: Abbreviation for “Sosa-Stradonitz number”. This is an individual’s surname, so may need transliterating into non-latin alphabets. */ 237 I18N::translate('Sosa') ?></th> 238 <th><?= I18N::translate('Birth') ?></th> 239 <th> 240 <span title="<?= I18N::translate('Anniversary') ?>"> 241 <?= view('icons/anniversary') ?> 242 </span> 243 </th> 244 <th><?= I18N::translate('Place') ?></th> 245 <th> 246 <i class="icon-children" title="<?= I18N::translate('Children') ?>"></i> 247 </th> 248 <th><?= I18N::translate('Death') ?></th> 249 <th> 250 <span title="<?= I18N::translate('Anniversary') ?>"> 251 <?= view('icons/anniversary') ?> 252 </span> 253 </th> 254 <th><?= I18N::translate('Age') ?></th> 255 <th><?= I18N::translate('Place') ?></th> 256 <th><?= I18N::translate('Last change') ?></th> 257 <th hidden></th> 258 <th hidden></th> 259 <th hidden></th> 260 <th hidden></th> 261 </tr> 262 </thead> 263 264 <tbody> 265 <?php foreach ($individuals as $key => $individual) : ?> 266 <tr class="<?= $individual->isPendingDeletion() ? 'wt-old' : ($individual->isPendingAddition() ? 'wt-new' : '') ?>"> 267 <td colspan="2" data-sort="<?= e(str_replace([',', Individual::PRAENOMEN_NESCIO, Individual::NOMEN_NESCIO], 'AAAA', implode(',', array_reverse(explode(',', $individual->sortName()))))) ?>"> 268 <?php foreach ($individual->getAllNames() as $num => $name) : ?> 269 <a title="<?= $name['type'] === 'NAME' ? '' : Registry::elementFactory()->make('INDI:NAME:' . $name['type'])->label() ?>" href="<?= e($individual->url()) ?>" class="<?= $num === $individual->getPrimaryName() ? 'name2' : '' ?>"> 270 <?= $name['full'] ?> 271 </a> 272 <?php if ($num === $individual->getPrimaryName()) : ?> 273 <small><?= view('icons/sex', ['sex' => $individual->sex()]) ?></small> 274 <?php endif ?> 275 <br> 276 <?php endforeach ?> 277 <?= view('lists/individual-table-parents', ['individual' => $individual]) ?> 278 </td> 279 280 <td hidden data-sort="<?= e(str_replace([',', Individual::PRAENOMEN_NESCIO, Individual::NOMEN_NESCIO], 'AAAA', $individual->sortName())) ?>"></td> 281 282 <td class="text-center" data-sort="<?= $key ?>"> 283 <?php if ($sosa) : ?> 284 <?php if ($module instanceof RelationshipsChartModule) : ?> 285 <a href="<?= e($module->chartUrl($individuals[1], ['xref2' => $individual->xref()])) ?>" rel="nofollow" title="<?= I18N::translate('Relationships') ?>" rel="nofollow"> 286 <?= I18N::number($key) ?> 287 </a> 288 <?php else : ?> 289 <?= I18N::number($key) ?> 290 <?php endif ?> 291 <?php endif ?> 292 </td> 293 294 <!-- Birth date --> 295 <?php $estimated_birth_date = $individual->getEstimatedBirthDate(); ?> 296 297 <td data-sort="<?= $estimated_birth_date->julianDay() ?>"> 298 <?php $birth_dates = $individual->getAllBirthDates(); ?> 299 300 <?php foreach ($birth_dates as $n => $birth_date) : ?> 301 <?= $birth_date->display(true) ?> 302 <br> 303 <?php endforeach ?> 304 305 <?php if (empty($birth_dates) && $show_estimated_dates) : ?> 306 <?= $estimated_birth_date->display(true) ?> 307 <?php endif ?> 308 </td> 309 310 <!-- Birth anniversary --> 311 <?php if (isset($birth_dates[0]) && $birth_dates[0]->gregorianYear() >= 1550 && $birth_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->xref()])) : ?> 312 <?php 313 ++$birt_by_decade[(int) ($birth_dates[0]->gregorianYear() / 10) * 10][$individual->sex()]; 314 ?> 315 <?php endif ?> 316 <td class="text-center" data-sort="<?= - $estimated_birth_date->julianDay() ?>"> 317 <?= (new Age($birth_dates[0] ?? new Date(''), $today))->ageYearsString() ?> 318 </td> 319 320 <!-- Birth place --> 321 <td data-sort="<?= e($individual->getBirthPlace()->gedcomName()) ?>"> 322 <?php foreach ($individual->getAllBirthPlaces() as $n => $birth_place) : ?> 323 <?= $birth_place->shortName(true) ?> 324 <br> 325 <?php endforeach ?> 326 </td> 327 328 <!-- Number of children --> 329 <td class="text-center" data-sort="<?= $individual->numberOfChildren() ?>"> 330 <?= I18N::number($individual->numberOfChildren()) ?> 331 </td> 332 333 <!-- Death date --> 334 <?php $death_dates = $individual->getAllDeathDates() ?> 335 <td data-sort="<?= $individual->getEstimatedDeathDate()->julianDay() ?>"> 336 <?php foreach ($death_dates as $num => $death_date) : ?> 337 <?= $death_date->display(true) ?> 338 <br> 339 <?php endforeach ?> 340 341 <?php if (empty($death_dates) && $show_estimated_dates && $individual->getEstimatedDeathDate()->minimumDate()->minimumJulianDay() < $today_jd) : ?> 342 <?= $individual->getEstimatedDeathDate()->display(true) ?> 343 <?php endif ?> 344 </td> 345 346 <!-- Death anniversary --> 347 <?php if (isset($death_dates[0]) && $death_dates[0]->gregorianYear() >= 1550 && $death_dates[0]->gregorianYear() < 2030 && !isset($unique_indis[$individual->xref()])) : ?> 348 <?php 349 ++$deat_by_decade[(int) ($death_dates[0]->gregorianYear() / 10) * 10][$individual->sex()]; 350 ?> 351 <?php endif ?> 352 <td class="text-center" data-sort="<?= - $individual->getEstimatedDeathDate()->julianDay() ?>"> 353 <?= (new Age($death_dates[0] ?? new Date(''), $today))->ageYearsString() ?> 354 </td> 355 356 <!-- Age at death --> 357 <?php $age = new Age($birth_dates[0] ?? new Date(''), $death_dates[0] ?? new Date('')) ?> 358 <?php if (!isset($unique_indis[$individual->xref()]) && $age->ageYears() >= 0 && $age->ageYears() <= $max_age) : ?> 359 <?php ++$deat_by_age[$age->ageYears()][$individual->sex()] ?> 360 <?php endif ?> 361 <td class="text-center" data-sort="<?= $age->ageDays() ?>"> 362 <?= $age->ageYearsString() ?> 363 </td> 364 365 <!-- Death place --> 366 <td data-sort="<?= e($individual->getDeathPlace()->gedcomName()) ?>"> 367 <?php foreach ($individual->getAllDeathPlaces() as $n => $death_place) : ?> 368 <?= $death_place->shortName(true) ?> 369 <br> 370 <?php endforeach ?> 371 </td> 372 373 <!-- Last change --> 374 <td data-sort="<?= $individual->lastChangeTimestamp()->unix() ?>"> 375 <?= view('components/datetime', ['timestamp' => $individual->lastChangeTimestamp()]) ?> 376 </td> 377 378 <!-- Filter by sex --> 379 <td hidden> 380 <?= $individual->sex() ?> 381 </td> 382 383 <!-- Filter by birth date --> 384 <td hidden> 385 <?php if ($estimated_birth_date->maximumJulianDay() > $hundred_years_ago && $estimated_birth_date->maximumJulianDay() <= $today_jd) : ?> 386 Y100 387 <?php else : ?> 388 YES 389 <?php endif ?> 390 </td> 391 392 <!-- Filter by death date --> 393 <td hidden> 394 <?php if ($individual->getEstimatedDeathDate()->maximumJulianDay() > $hundred_years_ago && $individual->getEstimatedDeathDate()->maximumJulianDay() <= $today_jd) : ?> 395 Y100 396 <?php elseif ($individual->isDead()) : ?> 397 YES 398 <?php else : ?> 399 N 400 <?php endif ?> 401 </td> 402 403 <!-- Filter by roots/leaves --> 404 <td hidden> 405 <?php if ($individual->childFamilies()->isEmpty()) : ?> 406 R 407 <?php elseif (!$individual->isDead() && $individual->numberOfChildren() < 1) : ?> 408 L 409 <?php endif ?> 410 </td> 411 </tr> 412 413 <?php $unique_indis[$individual->xref()] = true ?> 414 <?php endforeach ?> 415 </tbody> 416 417 <tfoot> 418 <tr> 419 <th colspan="16"> 420 <div class="btn-group btn-group-sm"> 421 <button id="btn-toggle-parents" class="btn btn-outline-secondary" data-toggle="button" data-persist="show-parents"> 422 <?= I18N::translate('Show parents') ?> 423 </button> 424 <button id="btn-toggle-statistics" class="btn btn-outline-secondary" data-toggle="button" data-persist="show-statistics"> 425 <?= I18N::translate('Show statistics charts') ?> 426 </button> 427 </div> 428 </th> 429 </tr> 430 </tfoot> 431 </table> 432</div> 433 434<div id="individual-charts-<?= e($table_id) ?>" style="display: none;"> 435 <div class="mb-3"> 436 <div class="card-deck"> 437 <div class="col-lg-12 col-md-12 mb-3"> 438 <div class="card m-0"> 439 <div class="card-header"> 440 <?= I18N::translate('Decade of birth') ?> 441 </div> 442 <div class="card-body"> 443 <?php 444 foreach ($birt_by_decade as $century => $values) { 445 if (($values['M'] + $values['F']) > 0) { 446 $birthData[] = [ 447 [ 448 'v' => 'Date(' . $century . ', 0, 1)', 449 'f' => $century, 450 ], 451 $values['M'], 452 $values['F'], 453 ]; 454 } 455 } 456 ?> 457 <?= view('lists/chart-by-decade', ['data' => $birthData, 'title' => I18N::translate('Decade of birth')]) ?> 458 </div> 459 </div> 460 </div> 461 </div> 462 <div class="card-deck"> 463 <div class="col-lg-12 col-md-12 mb-3"> 464 <div class="card m-0"> 465 <div class="card-header"> 466 <?= I18N::translate('Decade of death') ?> 467 </div> 468 <div class="card-body"> 469 <?php 470 foreach ($deat_by_decade as $century => $values) { 471 if (($values['M'] + $values['F']) > 0) { 472 $deathData[] = [ 473 [ 474 'v' => 'Date(' . $century . ', 0, 1)', 475 'f' => $century, 476 ], 477 $values['M'], 478 $values['F'], 479 ]; 480 } 481 } 482 ?> 483 <?= view('lists/chart-by-decade', ['data' => $deathData, 'title' => I18N::translate('Decade of death')]) ?> 484 </div> 485 </div> 486 </div> 487 </div> 488 <div class="card-deck"> 489 <div class="col-lg-12 col-md-12 mb-3"> 490 <div class="card m-0"> 491 <div class="card-header"> 492 <?= I18N::translate('Age related to death year') ?> 493 </div> 494 <div class="card-body"> 495 <?php 496 $totalAge = 0; 497 $totalSum = 0; 498 $max = 0; 499 500 foreach ($deat_by_age as $age => $values) { 501 if (($values['M'] + $values['F']) > 0) { 502 if (($values['M'] + $values['F']) > $max) { 503 $max = $values['M'] + $values['F']; 504 } 505 506 $totalAge += $age * ($values['M'] + $values['F']); 507 $totalSum += $values['M'] + $values['F']; 508 509 $deathAgeData[] = [ 510 $age, 511 $values['M'], 512 $values['F'], 513 null, 514 ]; 515 } 516 } 517 518 if ($totalSum > 0) { 519 $deathAgeData[] = [ 520 round($totalAge / $totalSum, 1), 521 null, 522 null, 523 0, 524 ]; 525 526 $deathAgeData[] = [ 527 round($totalAge / $totalSum, 1), 528 null, 529 null, 530 $max, 531 ]; 532 } 533 ?> 534 <?= view('lists/chart-by-age', ['data' => $deathAgeData, 'title' => I18N::translate('Age related to death year')]) ?> 535 </div> 536 </div> 537 </div> 538 </div> 539 </div> 540</div> 541