1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Statistics\Repository; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Gedcom; 24use Fisharebest\Webtrees\GedcomRecord; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Module\IndividualListModule; 28use Fisharebest\Webtrees\Module\ModuleInterface; 29use Fisharebest\Webtrees\Module\ModuleListInterface; 30use Fisharebest\Webtrees\Registry; 31use Fisharebest\Webtrees\Services\ModuleService; 32use Fisharebest\Webtrees\Statistics\Google\ChartAge; 33use Fisharebest\Webtrees\Statistics\Google\ChartBirth; 34use Fisharebest\Webtrees\Statistics\Google\ChartCommonGiven; 35use Fisharebest\Webtrees\Statistics\Google\ChartCommonSurname; 36use Fisharebest\Webtrees\Statistics\Google\ChartDeath; 37use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources; 38use Fisharebest\Webtrees\Statistics\Google\ChartIndividualWithSources; 39use Fisharebest\Webtrees\Statistics\Google\ChartMortality; 40use Fisharebest\Webtrees\Statistics\Google\ChartSex; 41use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface; 42use Fisharebest\Webtrees\Statistics\Service\CenturyService; 43use Fisharebest\Webtrees\Statistics\Service\ColorService; 44use Fisharebest\Webtrees\Tree; 45use Illuminate\Database\Capsule\Manager as DB; 46use Illuminate\Database\Query\Builder; 47use Illuminate\Database\Query\Expression; 48use Illuminate\Database\Query\JoinClause; 49use stdClass; 50 51use function array_key_exists; 52use function array_keys; 53use function array_reverse; 54use function array_shift; 55use function array_slice; 56use function array_walk; 57use function arsort; 58use function e; 59use function explode; 60use function implode; 61use function preg_match; 62use function uksort; 63use function view; 64 65/** 66 * A repository providing methods for individual related statistics. 67 */ 68class IndividualRepository implements IndividualRepositoryInterface 69{ 70 private CenturyService $century_service; 71 72 private ColorService $color_service; 73 74 private Tree $tree; 75 76 /** 77 * @param CenturyService $century_service 78 * @param ColorService $color_service 79 * @param Tree $tree 80 */ 81 public function __construct(CenturyService $century_service, ColorService $color_service, Tree $tree) 82 { 83 $this->century_service = $century_service; 84 $this->color_service = $color_service; 85 $this->tree = $tree; 86 } 87 88 /** 89 * Find common given names. 90 * 91 * @param string $sex 92 * @param string $type 93 * @param bool $show_tot 94 * @param int $threshold 95 * @param int $maxtoshow 96 * 97 * @return string|array<int> 98 */ 99 private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow) 100 { 101 $query = DB::table('name') 102 ->join('individuals', static function (JoinClause $join): void { 103 $join 104 ->on('i_file', '=', 'n_file') 105 ->on('i_id', '=', 'n_id'); 106 }) 107 ->where('n_file', '=', $this->tree->id()) 108 ->where('n_type', '<>', '_MARNM') 109 ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO) 110 ->where(new Expression('LENGTH(n_givn)'), '>', 1); 111 112 switch ($sex) { 113 case 'M': 114 case 'F': 115 case 'U': 116 $query->where('i_sex', '=', $sex); 117 break; 118 119 case 'B': 120 default: 121 $query->where('i_sex', '<>', 'U'); 122 break; 123 } 124 125 $rows = $query 126 ->groupBy(['n_givn']) 127 ->pluck(new Expression('COUNT(distinct n_id) AS count'), 'n_givn'); 128 129 $nameList = []; 130 131 foreach ($rows as $n_givn => $count) { 132 // Split “John Thomas” into “John” and “Thomas” and count against both totals 133 foreach (explode(' ', (string) $n_givn) as $given) { 134 // Exclude initials and particles. 135 if (preg_match('/^([A-Z]|[a-z]{1,3})$/', $given) !== 1) { 136 if (array_key_exists($given, $nameList)) { 137 $nameList[$given] += (int) $count; 138 } else { 139 $nameList[$given] = (int) $count; 140 } 141 } 142 } 143 } 144 arsort($nameList); 145 $nameList = array_slice($nameList, 0, $maxtoshow); 146 147 foreach ($nameList as $given => $total) { 148 if ($total < $threshold) { 149 unset($nameList[$given]); 150 } 151 } 152 153 switch ($type) { 154 case 'chart': 155 return $nameList; 156 157 case 'table': 158 return view('lists/given-names-table', [ 159 'given_names' => $nameList, 160 'order' => [[1, 'desc']], 161 ]); 162 163 case 'list': 164 return view('lists/given-names-list', [ 165 'given_names' => $nameList, 166 'show_totals' => $show_tot, 167 ]); 168 169 case 'nolist': 170 default: 171 array_walk($nameList, static function (string &$value, string $key) use ($show_tot): void { 172 if ($show_tot) { 173 $value = '<bdi>' . e($key) . '</bdi> (' . I18N::number((int) $value) . ')'; 174 } else { 175 $value = '<bdi>' . e($key) . '</bdi>'; 176 } 177 }); 178 179 return implode(I18N::$list_separator, $nameList); 180 } 181 } 182 183 /** 184 * Find common give names. 185 * 186 * @param int $threshold 187 * @param int $maxtoshow 188 * 189 * @return string 190 */ 191 public function commonGiven(int $threshold = 1, int $maxtoshow = 10): string 192 { 193 return $this->commonGivenQuery('B', 'nolist', false, $threshold, $maxtoshow); 194 } 195 196 /** 197 * Find common give names. 198 * 199 * @param int $threshold 200 * @param int $maxtoshow 201 * 202 * @return string 203 */ 204 public function commonGivenTotals(int $threshold = 1, int $maxtoshow = 10): string 205 { 206 return $this->commonGivenQuery('B', 'nolist', true, $threshold, $maxtoshow); 207 } 208 209 /** 210 * Find common give names. 211 * 212 * @param int $threshold 213 * @param int $maxtoshow 214 * 215 * @return string 216 */ 217 public function commonGivenList(int $threshold = 1, int $maxtoshow = 10): string 218 { 219 return $this->commonGivenQuery('B', 'list', false, $threshold, $maxtoshow); 220 } 221 222 /** 223 * Find common give names. 224 * 225 * @param int $threshold 226 * @param int $maxtoshow 227 * 228 * @return string 229 */ 230 public function commonGivenListTotals(int $threshold = 1, int $maxtoshow = 10): string 231 { 232 return $this->commonGivenQuery('B', 'list', true, $threshold, $maxtoshow); 233 } 234 235 /** 236 * Find common give names. 237 * 238 * @param int $threshold 239 * @param int $maxtoshow 240 * 241 * @return string 242 */ 243 public function commonGivenTable(int $threshold = 1, int $maxtoshow = 10): string 244 { 245 return $this->commonGivenQuery('B', 'table', false, $threshold, $maxtoshow); 246 } 247 248 /** 249 * Find common give names of females. 250 * 251 * @param int $threshold 252 * @param int $maxtoshow 253 * 254 * @return string 255 */ 256 public function commonGivenFemale(int $threshold = 1, int $maxtoshow = 10): string 257 { 258 return $this->commonGivenQuery('F', 'nolist', false, $threshold, $maxtoshow); 259 } 260 261 /** 262 * Find common give names of females. 263 * 264 * @param int $threshold 265 * @param int $maxtoshow 266 * 267 * @return string 268 */ 269 public function commonGivenFemaleTotals(int $threshold = 1, int $maxtoshow = 10): string 270 { 271 return $this->commonGivenQuery('F', 'nolist', true, $threshold, $maxtoshow); 272 } 273 274 /** 275 * Find common give names of females. 276 * 277 * @param int $threshold 278 * @param int $maxtoshow 279 * 280 * @return string 281 */ 282 public function commonGivenFemaleList(int $threshold = 1, int $maxtoshow = 10): string 283 { 284 return $this->commonGivenQuery('F', 'list', false, $threshold, $maxtoshow); 285 } 286 287 /** 288 * Find common give names of females. 289 * 290 * @param int $threshold 291 * @param int $maxtoshow 292 * 293 * @return string 294 */ 295 public function commonGivenFemaleListTotals(int $threshold = 1, int $maxtoshow = 10): string 296 { 297 return $this->commonGivenQuery('F', 'list', true, $threshold, $maxtoshow); 298 } 299 300 /** 301 * Find common give names of females. 302 * 303 * @param int $threshold 304 * @param int $maxtoshow 305 * 306 * @return string 307 */ 308 public function commonGivenFemaleTable(int $threshold = 1, int $maxtoshow = 10): string 309 { 310 return $this->commonGivenQuery('F', 'table', false, $threshold, $maxtoshow); 311 } 312 313 /** 314 * Find common give names of males. 315 * 316 * @param int $threshold 317 * @param int $maxtoshow 318 * 319 * @return string 320 */ 321 public function commonGivenMale(int $threshold = 1, int $maxtoshow = 10): string 322 { 323 return $this->commonGivenQuery('M', 'nolist', false, $threshold, $maxtoshow); 324 } 325 326 /** 327 * Find common give names of males. 328 * 329 * @param int $threshold 330 * @param int $maxtoshow 331 * 332 * @return string 333 */ 334 public function commonGivenMaleTotals(int $threshold = 1, int $maxtoshow = 10): string 335 { 336 return $this->commonGivenQuery('M', 'nolist', true, $threshold, $maxtoshow); 337 } 338 339 /** 340 * Find common give names of males. 341 * 342 * @param int $threshold 343 * @param int $maxtoshow 344 * 345 * @return string 346 */ 347 public function commonGivenMaleList(int $threshold = 1, int $maxtoshow = 10): string 348 { 349 return $this->commonGivenQuery('M', 'list', false, $threshold, $maxtoshow); 350 } 351 352 /** 353 * Find common give names of males. 354 * 355 * @param int $threshold 356 * @param int $maxtoshow 357 * 358 * @return string 359 */ 360 public function commonGivenMaleListTotals(int $threshold = 1, int $maxtoshow = 10): string 361 { 362 return $this->commonGivenQuery('M', 'list', true, $threshold, $maxtoshow); 363 } 364 365 /** 366 * Find common give names of males. 367 * 368 * @param int $threshold 369 * @param int $maxtoshow 370 * 371 * @return string 372 */ 373 public function commonGivenMaleTable(int $threshold = 1, int $maxtoshow = 10): string 374 { 375 return $this->commonGivenQuery('M', 'table', false, $threshold, $maxtoshow); 376 } 377 378 /** 379 * Find common give names of unknown sexes. 380 * 381 * @param int $threshold 382 * @param int $maxtoshow 383 * 384 * @return string 385 */ 386 public function commonGivenUnknown(int $threshold = 1, int $maxtoshow = 10): string 387 { 388 return $this->commonGivenQuery('U', 'nolist', false, $threshold, $maxtoshow); 389 } 390 391 /** 392 * Find common give names of unknown sexes. 393 * 394 * @param int $threshold 395 * @param int $maxtoshow 396 * 397 * @return string 398 */ 399 public function commonGivenUnknownTotals(int $threshold = 1, int $maxtoshow = 10): string 400 { 401 return $this->commonGivenQuery('U', 'nolist', true, $threshold, $maxtoshow); 402 } 403 404 /** 405 * Find common give names of unknown sexes. 406 * 407 * @param int $threshold 408 * @param int $maxtoshow 409 * 410 * @return string 411 */ 412 public function commonGivenUnknownList(int $threshold = 1, int $maxtoshow = 10): string 413 { 414 return $this->commonGivenQuery('U', 'list', false, $threshold, $maxtoshow); 415 } 416 417 /** 418 * Find common give names of unknown sexes. 419 * 420 * @param int $threshold 421 * @param int $maxtoshow 422 * 423 * @return string 424 */ 425 public function commonGivenUnknownListTotals(int $threshold = 1, int $maxtoshow = 10): string 426 { 427 return $this->commonGivenQuery('U', 'list', true, $threshold, $maxtoshow); 428 } 429 430 /** 431 * Find common give names of unknown sexes. 432 * 433 * @param int $threshold 434 * @param int $maxtoshow 435 * 436 * @return string 437 */ 438 public function commonGivenUnknownTable(int $threshold = 1, int $maxtoshow = 10): string 439 { 440 return $this->commonGivenQuery('U', 'table', false, $threshold, $maxtoshow); 441 } 442 443 /** 444 * Count the number of distinct given names (or the number of occurences of specific given names). 445 * 446 * @param array<string> ...$params 447 * 448 * @return string 449 */ 450 public function totalGivennames(...$params): string 451 { 452 $query = DB::table('name') 453 ->where('n_file', '=', $this->tree->id()); 454 455 if ($params === []) { 456 // Count number of distinct given names. 457 $query 458 ->distinct() 459 ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO) 460 ->whereNotNull('n_givn'); 461 } else { 462 // Count number of occurences of specific given names. 463 $query->whereIn('n_givn', $params); 464 } 465 466 $count = $query->count('n_givn'); 467 468 return I18N::number($count); 469 } 470 471 /** 472 * Count the number of distinct surnames (or the number of occurrences of specific surnames). 473 * 474 * @param array<string> ...$params 475 * 476 * @return string 477 */ 478 public function totalSurnames(...$params): string 479 { 480 $query = DB::table('name') 481 ->where('n_file', '=', $this->tree->id()); 482 483 if ($params === []) { 484 // Count number of distinct surnames 485 $query->distinct() 486 ->whereNotNull('n_surn'); 487 } else { 488 // Count number of occurences of specific surnames. 489 $query->whereIn('n_surn', $params); 490 } 491 492 $count = $query->count('n_surn'); 493 494 return I18N::number($count); 495 } 496 497 /** 498 * @param int $number_of_surnames 499 * @param int $threshold 500 * 501 * @return array<array<int>> 502 */ 503 private function topSurnames(int $number_of_surnames, int $threshold): array 504 { 505 // Use the count of base surnames. 506 $top_surnames = DB::table('name') 507 ->where('n_file', '=', $this->tree->id()) 508 ->where('n_type', '<>', '_MARNM') 509 ->whereNotIn('n_surn', ['', Individual::NOMEN_NESCIO]) 510 ->select(['n_surn']) 511 ->groupBy(['n_surn']) 512 ->orderByRaw('COUNT(n_surn) DESC') 513 ->orderBy(new Expression('COUNT(n_surn)'), 'DESC') 514 ->having(new Expression('COUNT(n_surn)'), '>=', $threshold) 515 ->take($number_of_surnames) 516 ->get() 517 ->pluck('n_surn') 518 ->all(); 519 520 $surnames = []; 521 522 foreach ($top_surnames as $top_surname) { 523 $surnames[$top_surname] = DB::table('name') 524 ->where('n_file', '=', $this->tree->id()) 525 ->where('n_type', '<>', '_MARNM') 526 ->where('n_surn', '=', $top_surname) 527 ->select(['n_surn', new Expression('COUNT(n_surn) AS count')]) 528 ->groupBy(['n_surn']) 529 ->orderBy('n_surn') 530 ->get() 531 ->pluck('count', 'n_surn') 532 ->map(static fn (string $count): int => (int) $count) 533 ->all(); 534 } 535 536 return $surnames; 537 } 538 539 /** 540 * Find common surnames. 541 * 542 * @return string 543 */ 544 public function getCommonSurname(): string 545 { 546 $top_surname = $this->topSurnames(1, 0); 547 548 return implode(', ', array_keys(array_shift($top_surname) ?? [])); 549 } 550 551 /** 552 * Find common surnames. 553 * 554 * @param string $type 555 * @param bool $show_tot 556 * @param int $threshold 557 * @param int $number_of_surnames 558 * @param string $sorting 559 * 560 * @return string 561 */ 562 private function commonSurnamesQuery( 563 string $type, 564 bool $show_tot, 565 int $threshold, 566 int $number_of_surnames, 567 string $sorting 568 ): string { 569 $surnames = $this->topSurnames($number_of_surnames, $threshold); 570 571 switch ($sorting) { 572 default: 573 case 'alpha': 574 uksort($surnames, I18N::comparator()); 575 break; 576 case 'count': 577 break; 578 case 'rcount': 579 $surnames = array_reverse($surnames, true); 580 break; 581 } 582 583 // find a module providing individual lists 584 $module_service = Registry::container()->get(ModuleService::class); 585 586 $module = $module_service 587 ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user()) 588 ->first(static fn (ModuleInterface $module): bool => $module instanceof IndividualListModule); 589 590 if ($type === 'list') { 591 return view('lists/surnames-bullet-list', [ 592 'surnames' => $surnames, 593 'module' => $module, 594 'totals' => $show_tot, 595 'tree' => $this->tree, 596 ]); 597 } 598 599 return view('lists/surnames-compact-list', [ 600 'surnames' => $surnames, 601 'module' => $module, 602 'totals' => $show_tot, 603 'tree' => $this->tree, 604 ]); 605 } 606 607 /** 608 * Find common surnames. 609 * 610 * @param int $threshold 611 * @param int $number_of_surnames 612 * @param string $sorting 613 * 614 * @return string 615 */ 616 public function commonSurnames( 617 int $threshold = 1, 618 int $number_of_surnames = 10, 619 string $sorting = 'alpha' 620 ): string { 621 return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting); 622 } 623 624 /** 625 * Find common surnames. 626 * 627 * @param int $threshold 628 * @param int $number_of_surnames 629 * @param string $sorting 630 * 631 * @return string 632 */ 633 public function commonSurnamesTotals( 634 int $threshold = 1, 635 int $number_of_surnames = 10, 636 string $sorting = 'count' 637 ): string { 638 return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting); 639 } 640 641 /** 642 * Find common surnames. 643 * 644 * @param int $threshold 645 * @param int $number_of_surnames 646 * @param string $sorting 647 * 648 * @return string 649 */ 650 public function commonSurnamesList( 651 int $threshold = 1, 652 int $number_of_surnames = 10, 653 string $sorting = 'alpha' 654 ): string { 655 return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting); 656 } 657 658 /** 659 * Find common surnames. 660 * 661 * @param int $threshold 662 * @param int $number_of_surnames 663 * @param string $sorting 664 * 665 * @return string 666 */ 667 public function commonSurnamesListTotals( 668 int $threshold = 1, 669 int $number_of_surnames = 10, 670 string $sorting = 'count' 671 ): string { 672 return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting); 673 } 674 675 /** 676 * Get a count of births by month. 677 * 678 * @param int $year1 679 * @param int $year2 680 * 681 * @return Builder 682 */ 683 public function statsBirthQuery(int $year1 = -1, int $year2 = -1): Builder 684 { 685 $query = DB::table('dates') 686 ->select(['d_month', new Expression('COUNT(*) AS total')]) 687 ->where('d_file', '=', $this->tree->id()) 688 ->where('d_fact', '=', 'BIRT') 689 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 690 ->groupBy(['d_month']); 691 692 if ($year1 >= 0 && $year2 >= 0) { 693 $query->whereBetween('d_year', [$year1, $year2]); 694 } 695 696 return $query; 697 } 698 699 /** 700 * Get a count of births by month. 701 * 702 * @param int $year1 703 * @param int $year2 704 * 705 * @return Builder 706 */ 707 public function statsBirthBySexQuery(int $year1 = -1, int $year2 = -1): Builder 708 { 709 return $this->statsBirthQuery($year1, $year2) 710 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 711 ->join('individuals', static function (JoinClause $join): void { 712 $join 713 ->on('i_id', '=', 'd_gid') 714 ->on('i_file', '=', 'd_file'); 715 }) 716 ->groupBy(['i_sex']); 717 } 718 719 /** 720 * General query on births. 721 * 722 * @param string|null $color_from 723 * @param string|null $color_to 724 * 725 * @return string 726 */ 727 public function statsBirth(string $color_from = null, string $color_to = null): string 728 { 729 return (new ChartBirth($this->century_service, $this->color_service, $this->tree)) 730 ->chartBirth($color_from, $color_to); 731 } 732 733 /** 734 * Get a list of death dates. 735 * 736 * @param int $year1 737 * @param int $year2 738 * 739 * @return Builder 740 */ 741 public function statsDeathQuery(int $year1 = -1, int $year2 = -1): Builder 742 { 743 $query = DB::table('dates') 744 ->select(['d_month', new Expression('COUNT(*) AS total')]) 745 ->where('d_file', '=', $this->tree->id()) 746 ->where('d_fact', '=', 'DEAT') 747 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 748 ->groupBy(['d_month']); 749 750 if ($year1 >= 0 && $year2 >= 0) { 751 $query->whereBetween('d_year', [$year1, $year2]); 752 } 753 754 return $query; 755 } 756 757 /** 758 * Get a list of death dates. 759 * 760 * @param int $year1 761 * @param int $year2 762 * 763 * @return Builder 764 */ 765 public function statsDeathBySexQuery(int $year1 = -1, int $year2 = -1): Builder 766 { 767 return $this->statsDeathQuery($year1, $year2) 768 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 769 ->join('individuals', static function (JoinClause $join): void { 770 $join 771 ->on('i_id', '=', 'd_gid') 772 ->on('i_file', '=', 'd_file'); 773 }) 774 ->groupBy(['i_sex']); 775 } 776 777 /** 778 * General query on deaths. 779 * 780 * @param string|null $color_from 781 * @param string|null $color_to 782 * 783 * @return string 784 */ 785 public function statsDeath(string $color_from = null, string $color_to = null): string 786 { 787 return (new ChartDeath($this->century_service, $this->color_service, $this->tree)) 788 ->chartDeath($color_from, $color_to); 789 } 790 791 /** 792 * General query on ages. 793 * 794 * @param string $related 795 * @param string $sex 796 * @param int $year1 797 * @param int $year2 798 * 799 * @return array<stdClass> 800 */ 801 public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array 802 { 803 $prefix = DB::connection()->getTablePrefix(); 804 805 $query = $this->birthAndDeathQuery($sex); 806 807 if ($year1 >= 0 && $year2 >= 0) { 808 $query 809 ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 810 ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']); 811 812 if ($related === 'BIRT') { 813 $query->whereBetween('birth.d_year', [$year1, $year2]); 814 } elseif ($related === 'DEAT') { 815 $query->whereBetween('death.d_year', [$year1, $year2]); 816 } 817 } 818 819 return $query 820 ->select([new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')]) 821 ->orderBy('days', 'desc') 822 ->get() 823 ->all(); 824 } 825 826 /** 827 * General query on ages. 828 * 829 * @return string 830 */ 831 public function statsAge(): string 832 { 833 return (new ChartAge($this->century_service, $this->tree))->chartAge(); 834 } 835 836 /** 837 * Lifespan 838 * 839 * @param string $type 840 * @param string $sex 841 * 842 * @return string 843 */ 844 private function longlifeQuery(string $type, string $sex): string 845 { 846 $prefix = DB::connection()->getTablePrefix(); 847 848 $row = $this->birthAndDeathQuery($sex) 849 ->orderBy('days', 'desc') 850 ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')]) 851 ->first(); 852 853 if ($row === null) { 854 return ''; 855 } 856 857 /** @var Individual $individual */ 858 $individual = Registry::individualFactory()->mapper($this->tree)($row); 859 860 if ($type !== 'age' && !$individual->canShow()) { 861 return I18N::translate('This information is private and cannot be shown.'); 862 } 863 864 switch ($type) { 865 default: 866 case 'full': 867 return $individual->formatList(); 868 869 case 'age': 870 return I18N::number((int) ($row->days / 365.25)); 871 872 case 'name': 873 return '<a href="' . e($individual->url()) . '">' . $individual->fullName() . '</a>'; 874 } 875 } 876 877 /** 878 * Find the longest lived individual. 879 * 880 * @return string 881 */ 882 public function longestLife(): string 883 { 884 return $this->longlifeQuery('full', 'BOTH'); 885 } 886 887 /** 888 * Find the age of the longest lived individual. 889 * 890 * @return string 891 */ 892 public function longestLifeAge(): string 893 { 894 return $this->longlifeQuery('age', 'BOTH'); 895 } 896 897 /** 898 * Find the name of the longest lived individual. 899 * 900 * @return string 901 */ 902 public function longestLifeName(): string 903 { 904 return $this->longlifeQuery('name', 'BOTH'); 905 } 906 907 /** 908 * Find the longest lived female. 909 * 910 * @return string 911 */ 912 public function longestLifeFemale(): string 913 { 914 return $this->longlifeQuery('full', 'F'); 915 } 916 917 /** 918 * Find the age of the longest lived female. 919 * 920 * @return string 921 */ 922 public function longestLifeFemaleAge(): string 923 { 924 return $this->longlifeQuery('age', 'F'); 925 } 926 927 /** 928 * Find the name of the longest lived female. 929 * 930 * @return string 931 */ 932 public function longestLifeFemaleName(): string 933 { 934 return $this->longlifeQuery('name', 'F'); 935 } 936 937 /** 938 * Find the longest lived male. 939 * 940 * @return string 941 */ 942 public function longestLifeMale(): string 943 { 944 return $this->longlifeQuery('full', 'M'); 945 } 946 947 /** 948 * Find the age of the longest lived male. 949 * 950 * @return string 951 */ 952 public function longestLifeMaleAge(): string 953 { 954 return $this->longlifeQuery('age', 'M'); 955 } 956 957 /** 958 * Find the name of the longest lived male. 959 * 960 * @return string 961 */ 962 public function longestLifeMaleName(): string 963 { 964 return $this->longlifeQuery('name', 'M'); 965 } 966 967 /** 968 * Returns the calculated age the time of event. 969 * 970 * @param int $days The age from the database record 971 * 972 * @return string 973 */ 974 private function calculateAge(int $days): string 975 { 976 if ($days < 31) { 977 return I18N::plural('%s day', '%s days', $days, I18N::number($days)); 978 } 979 980 if ($days < 365) { 981 $months = (int) ($days / 30.5); 982 return I18N::plural('%s month', '%s months', $months, I18N::number($months)); 983 } 984 985 $years = (int) ($days / 365.25); 986 987 return I18N::plural('%s year', '%s years', $years, I18N::number($years)); 988 } 989 990 /** 991 * Find the oldest individuals. 992 * 993 * @param string $sex 994 * @param int $total 995 * 996 * @return array<array<string,mixed>> 997 */ 998 private function topTenOldestQuery(string $sex, int $total): array 999 { 1000 $prefix = DB::connection()->getTablePrefix(); 1001 1002 $rows = $this->birthAndDeathQuery($sex) 1003 ->groupBy(['i_id', 'i_file']) 1004 ->orderBy('days', 'desc') 1005 ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')]) 1006 ->take($total) 1007 ->get(); 1008 1009 $top10 = []; 1010 foreach ($rows as $row) { 1011 /** @var Individual $individual */ 1012 $individual = Registry::individualFactory()->mapper($this->tree)($row); 1013 1014 if ($individual->canShow()) { 1015 $top10[] = [ 1016 'person' => $individual, 1017 'age' => $this->calculateAge((int) $row->days), 1018 ]; 1019 } 1020 } 1021 1022 return $top10; 1023 } 1024 1025 /** 1026 * Find the oldest individuals. 1027 * 1028 * @param int $total 1029 * 1030 * @return string 1031 */ 1032 public function topTenOldest(int $total = 10): string 1033 { 1034 $records = $this->topTenOldestQuery('BOTH', $total); 1035 1036 return view('statistics/individuals/top10-nolist', [ 1037 'records' => $records, 1038 ]); 1039 } 1040 1041 /** 1042 * Find the oldest living individuals. 1043 * 1044 * @param int $total 1045 * 1046 * @return string 1047 */ 1048 public function topTenOldestList(int $total = 10): string 1049 { 1050 $records = $this->topTenOldestQuery('BOTH', $total); 1051 1052 return view('statistics/individuals/top10-list', [ 1053 'records' => $records, 1054 ]); 1055 } 1056 1057 /** 1058 * Find the oldest females. 1059 * 1060 * @param int $total 1061 * 1062 * @return string 1063 */ 1064 public function topTenOldestFemale(int $total = 10): string 1065 { 1066 $records = $this->topTenOldestQuery('F', $total); 1067 1068 return view('statistics/individuals/top10-nolist', [ 1069 'records' => $records, 1070 ]); 1071 } 1072 1073 /** 1074 * Find the oldest living females. 1075 * 1076 * @param int $total 1077 * 1078 * @return string 1079 */ 1080 public function topTenOldestFemaleList(int $total = 10): string 1081 { 1082 $records = $this->topTenOldestQuery('F', $total); 1083 1084 return view('statistics/individuals/top10-list', [ 1085 'records' => $records, 1086 ]); 1087 } 1088 1089 /** 1090 * Find the longest lived males. 1091 * 1092 * @param int $total 1093 * 1094 * @return string 1095 */ 1096 public function topTenOldestMale(int $total = 10): string 1097 { 1098 $records = $this->topTenOldestQuery('M', $total); 1099 1100 return view('statistics/individuals/top10-nolist', [ 1101 'records' => $records, 1102 ]); 1103 } 1104 1105 /** 1106 * Find the longest lived males. 1107 * 1108 * @param int $total 1109 * 1110 * @return string 1111 */ 1112 public function topTenOldestMaleList(int $total = 10): string 1113 { 1114 $records = $this->topTenOldestQuery('M', $total); 1115 1116 return view('statistics/individuals/top10-list', [ 1117 'records' => $records, 1118 ]); 1119 } 1120 1121 /** 1122 * Find the oldest living individuals. 1123 * 1124 * @param string $sex "M", "F" or "BOTH" 1125 * @param int $total 1126 * 1127 * @return array<array<string,mixed>> 1128 */ 1129 private function topTenOldestAliveQuery(string $sex, int $total): array 1130 { 1131 $query = DB::table('dates') 1132 ->join('individuals', static function (JoinClause $join): void { 1133 $join 1134 ->on('i_id', '=', 'd_gid') 1135 ->on('i_file', '=', 'd_file'); 1136 }) 1137 ->where('d_file', '=', $this->tree->id()) 1138 ->where('d_julianday1', '<>', 0) 1139 ->where('d_fact', '=', 'BIRT') 1140 ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%") 1141 ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%") 1142 ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%"); 1143 1144 if ($sex === 'F' || $sex === 'M') { 1145 $query->where('i_sex', '=', $sex); 1146 } 1147 1148 return $query 1149 ->groupBy(['i_id', 'i_file']) 1150 ->orderBy(new Expression('MIN(d_julianday1)')) 1151 ->select(['individuals.*']) 1152 ->take($total) 1153 ->get() 1154 ->map(Registry::individualFactory()->mapper($this->tree)) 1155 ->filter(GedcomRecord::accessFilter()) 1156 ->map(function (Individual $individual): array { 1157 return [ 1158 'person' => $individual, 1159 'age' => $this->calculateAge(Registry::timestampFactory()->now()->julianDay() - $individual->getBirthDate()->minimumJulianDay()), 1160 ]; 1161 }) 1162 ->all(); 1163 } 1164 1165 /** 1166 * Find the oldest living individuals. 1167 * 1168 * @param int $total 1169 * 1170 * @return string 1171 */ 1172 public function topTenOldestAlive(int $total = 10): string 1173 { 1174 if (!Auth::isMember($this->tree)) { 1175 return I18N::translate('This information is private and cannot be shown.'); 1176 } 1177 1178 $records = $this->topTenOldestAliveQuery('BOTH', $total); 1179 1180 return view('statistics/individuals/top10-nolist', [ 1181 'records' => $records, 1182 ]); 1183 } 1184 1185 /** 1186 * Find the oldest living individuals. 1187 * 1188 * @param int $total 1189 * 1190 * @return string 1191 */ 1192 public function topTenOldestListAlive(int $total = 10): string 1193 { 1194 if (!Auth::isMember($this->tree)) { 1195 return I18N::translate('This information is private and cannot be shown.'); 1196 } 1197 1198 $records = $this->topTenOldestAliveQuery('BOTH', $total); 1199 1200 return view('statistics/individuals/top10-list', [ 1201 'records' => $records, 1202 ]); 1203 } 1204 1205 /** 1206 * Find the oldest living females. 1207 * 1208 * @param int $total 1209 * 1210 * @return string 1211 */ 1212 public function topTenOldestFemaleAlive(int $total = 10): string 1213 { 1214 if (!Auth::isMember($this->tree)) { 1215 return I18N::translate('This information is private and cannot be shown.'); 1216 } 1217 1218 $records = $this->topTenOldestAliveQuery('F', $total); 1219 1220 return view('statistics/individuals/top10-nolist', [ 1221 'records' => $records, 1222 ]); 1223 } 1224 1225 /** 1226 * Find the oldest living females. 1227 * 1228 * @param int $total 1229 * 1230 * @return string 1231 */ 1232 public function topTenOldestFemaleListAlive(int $total = 10): string 1233 { 1234 if (!Auth::isMember($this->tree)) { 1235 return I18N::translate('This information is private and cannot be shown.'); 1236 } 1237 1238 $records = $this->topTenOldestAliveQuery('F', $total); 1239 1240 return view('statistics/individuals/top10-list', [ 1241 'records' => $records, 1242 ]); 1243 } 1244 1245 /** 1246 * Find the longest lived living males. 1247 * 1248 * @param int $total 1249 * 1250 * @return string 1251 */ 1252 public function topTenOldestMaleAlive(int $total = 10): string 1253 { 1254 if (!Auth::isMember($this->tree)) { 1255 return I18N::translate('This information is private and cannot be shown.'); 1256 } 1257 1258 $records = $this->topTenOldestAliveQuery('M', $total); 1259 1260 return view('statistics/individuals/top10-nolist', [ 1261 'records' => $records, 1262 ]); 1263 } 1264 1265 /** 1266 * Find the longest lived living males. 1267 * 1268 * @param int $total 1269 * 1270 * @return string 1271 */ 1272 public function topTenOldestMaleListAlive(int $total = 10): string 1273 { 1274 if (!Auth::isMember($this->tree)) { 1275 return I18N::translate('This information is private and cannot be shown.'); 1276 } 1277 1278 $records = $this->topTenOldestAliveQuery('M', $total); 1279 1280 return view('statistics/individuals/top10-list', [ 1281 'records' => $records, 1282 ]); 1283 } 1284 1285 /** 1286 * Find the average lifespan. 1287 * 1288 * @param string $sex "M", "F" or "BOTH" 1289 * @param bool $show_years 1290 * 1291 * @return string 1292 */ 1293 private function averageLifespanQuery(string $sex, bool $show_years): string 1294 { 1295 $prefix = DB::connection()->getTablePrefix(); 1296 1297 $days = (int) $this->birthAndDeathQuery($sex) 1298 ->select([new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')]) 1299 ->value('days'); 1300 1301 if ($show_years) { 1302 return $this->calculateAge($days); 1303 } 1304 1305 return I18N::number((int) ($days / 365.25)); 1306 } 1307 1308 /** 1309 * Find the average lifespan. 1310 * 1311 * @param bool $show_years 1312 * 1313 * @return string 1314 */ 1315 public function averageLifespan(bool $show_years): string 1316 { 1317 return $this->averageLifespanQuery('BOTH', $show_years); 1318 } 1319 1320 /** 1321 * Find the average lifespan of females. 1322 * 1323 * @param bool $show_years 1324 * 1325 * @return string 1326 */ 1327 public function averageLifespanFemale(bool $show_years): string 1328 { 1329 return $this->averageLifespanQuery('F', $show_years); 1330 } 1331 1332 /** 1333 * Find the average male lifespan. 1334 * 1335 * @param bool $show_years 1336 * 1337 * @return string 1338 */ 1339 public function averageLifespanMale(bool $show_years): string 1340 { 1341 return $this->averageLifespanQuery('M', $show_years); 1342 } 1343 1344 /** 1345 * Convert totals into percentages. 1346 * 1347 * @param int $count 1348 * @param int $total 1349 * 1350 * @return string 1351 */ 1352 private function getPercentage(int $count, int $total): string 1353 { 1354 return $total !== 0 ? I18N::percentage($count / $total, 1) : ''; 1355 } 1356 1357 /** 1358 * Returns how many individuals exist in the tree. 1359 * 1360 * @return int 1361 */ 1362 private function totalIndividualsQuery(): int 1363 { 1364 return DB::table('individuals') 1365 ->where('i_file', '=', $this->tree->id()) 1366 ->count(); 1367 } 1368 1369 /** 1370 * Count the number of living individuals. 1371 * 1372 * The totalLiving/totalDeceased queries assume that every dead person will 1373 * have a DEAT record. It will not include individuals who were born more 1374 * than MAX_ALIVE_AGE years ago, and who have no DEAT record. 1375 * A good reason to run the “Add missing DEAT records” batch-update! 1376 * 1377 * @return int 1378 */ 1379 private function totalLivingQuery(): int 1380 { 1381 $query = DB::table('individuals') 1382 ->where('i_file', '=', $this->tree->id()); 1383 1384 foreach (Gedcom::DEATH_EVENTS as $death_event) { 1385 $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%'); 1386 } 1387 1388 return $query->count(); 1389 } 1390 1391 /** 1392 * Count the number of dead individuals. 1393 * 1394 * @return int 1395 */ 1396 private function totalDeceasedQuery(): int 1397 { 1398 return DB::table('individuals') 1399 ->where('i_file', '=', $this->tree->id()) 1400 ->where(static function (Builder $query): void { 1401 foreach (Gedcom::DEATH_EVENTS as $death_event) { 1402 $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%'); 1403 } 1404 }) 1405 ->count(); 1406 } 1407 1408 /** 1409 * Returns the total count of a specific sex. 1410 * 1411 * @param string $sex The sex to query 1412 * 1413 * @return int 1414 */ 1415 private function getTotalSexQuery(string $sex): int 1416 { 1417 return DB::table('individuals') 1418 ->where('i_file', '=', $this->tree->id()) 1419 ->where('i_sex', '=', $sex) 1420 ->count(); 1421 } 1422 1423 /** 1424 * Returns the total number of males. 1425 * 1426 * @return int 1427 */ 1428 private function totalSexMalesQuery(): int 1429 { 1430 return $this->getTotalSexQuery('M'); 1431 } 1432 1433 /** 1434 * Returns the total number of females. 1435 * 1436 * @return int 1437 */ 1438 private function totalSexFemalesQuery(): int 1439 { 1440 return $this->getTotalSexQuery('F'); 1441 } 1442 1443 /** 1444 * Returns the total number of individuals with unknown sex. 1445 * 1446 * @return int 1447 */ 1448 private function totalSexUnknownQuery(): int 1449 { 1450 return $this->getTotalSexQuery('U'); 1451 } 1452 1453 /** 1454 * Count the total families. 1455 * 1456 * @return int 1457 */ 1458 private function totalFamiliesQuery(): int 1459 { 1460 return DB::table('families') 1461 ->where('f_file', '=', $this->tree->id()) 1462 ->count(); 1463 } 1464 1465 /** 1466 * How many individuals have one or more sources. 1467 * 1468 * @return int 1469 */ 1470 private function totalIndisWithSourcesQuery(): int 1471 { 1472 return DB::table('individuals') 1473 ->select(['i_id']) 1474 ->distinct() 1475 ->join('link', static function (JoinClause $join): void { 1476 $join->on('i_id', '=', 'l_from') 1477 ->on('i_file', '=', 'l_file'); 1478 }) 1479 ->where('l_file', '=', $this->tree->id()) 1480 ->where('l_type', '=', 'SOUR') 1481 ->count('i_id'); 1482 } 1483 1484 /** 1485 * Count the families with source records. 1486 * 1487 * @return int 1488 */ 1489 private function totalFamsWithSourcesQuery(): int 1490 { 1491 return DB::table('families') 1492 ->select(['f_id']) 1493 ->distinct() 1494 ->join('link', static function (JoinClause $join): void { 1495 $join->on('f_id', '=', 'l_from') 1496 ->on('f_file', '=', 'l_file'); 1497 }) 1498 ->where('l_file', '=', $this->tree->id()) 1499 ->where('l_type', '=', 'SOUR') 1500 ->count('f_id'); 1501 } 1502 1503 /** 1504 * Count the number of repositories. 1505 * 1506 * @return int 1507 */ 1508 private function totalRepositoriesQuery(): int 1509 { 1510 return DB::table('other') 1511 ->where('o_file', '=', $this->tree->id()) 1512 ->where('o_type', '=', 'REPO') 1513 ->count(); 1514 } 1515 1516 /** 1517 * Count the total number of sources. 1518 * 1519 * @return int 1520 */ 1521 private function totalSourcesQuery(): int 1522 { 1523 return DB::table('sources') 1524 ->where('s_file', '=', $this->tree->id()) 1525 ->count(); 1526 } 1527 1528 /** 1529 * Count the number of notes. 1530 * 1531 * @return int 1532 */ 1533 private function totalNotesQuery(): int 1534 { 1535 return DB::table('other') 1536 ->where('o_file', '=', $this->tree->id()) 1537 ->where('o_type', '=', 'NOTE') 1538 ->count(); 1539 } 1540 1541 /** 1542 * Count the total media. 1543 * 1544 * @return int 1545 */ 1546 private function totalMediaQuery(): int 1547 { 1548 return DB::table('media') 1549 ->where('m_file', '=', $this->tree->id()) 1550 ->count(); 1551 } 1552 1553 /** 1554 * Returns the total number of records. 1555 * 1556 * @return int 1557 */ 1558 private function totalRecordsQuery(): int 1559 { 1560 return $this->totalIndividualsQuery() 1561 + $this->totalFamiliesQuery() 1562 + $this->totalMediaQuery() 1563 + $this->totalNotesQuery() 1564 + $this->totalRepositoriesQuery() 1565 + $this->totalSourcesQuery(); 1566 } 1567 1568 /** 1569 * @return string 1570 */ 1571 public function totalRecords(): string 1572 { 1573 return I18N::number($this->totalRecordsQuery()); 1574 } 1575 1576 /** 1577 * @return string 1578 */ 1579 public function totalIndividuals(): string 1580 { 1581 return I18N::number($this->totalIndividualsQuery()); 1582 } 1583 1584 /** 1585 * Count the number of living individuals. 1586 * 1587 * @return string 1588 */ 1589 public function totalLiving(): string 1590 { 1591 return I18N::number($this->totalLivingQuery()); 1592 } 1593 1594 /** 1595 * Count the number of dead individuals. 1596 * 1597 * @return string 1598 */ 1599 public function totalDeceased(): string 1600 { 1601 return I18N::number($this->totalDeceasedQuery()); 1602 } 1603 1604 /** 1605 * @return string 1606 */ 1607 public function totalSexMales(): string 1608 { 1609 return I18N::number($this->totalSexMalesQuery()); 1610 } 1611 1612 /** 1613 * @return string 1614 */ 1615 public function totalSexFemales(): string 1616 { 1617 return I18N::number($this->totalSexFemalesQuery()); 1618 } 1619 1620 /** 1621 * @return string 1622 */ 1623 public function totalSexUnknown(): string 1624 { 1625 return I18N::number($this->totalSexUnknownQuery()); 1626 } 1627 1628 /** 1629 * @return string 1630 */ 1631 public function totalFamilies(): string 1632 { 1633 return I18N::number($this->totalFamiliesQuery()); 1634 } 1635 1636 /** 1637 * How many individuals have one or more sources. 1638 * 1639 * @return string 1640 */ 1641 public function totalIndisWithSources(): string 1642 { 1643 return I18N::number($this->totalIndisWithSourcesQuery()); 1644 } 1645 1646 /** 1647 * Count the families with with source records. 1648 * 1649 * @return string 1650 */ 1651 public function totalFamsWithSources(): string 1652 { 1653 return I18N::number($this->totalFamsWithSourcesQuery()); 1654 } 1655 1656 /** 1657 * @return string 1658 */ 1659 public function totalRepositories(): string 1660 { 1661 return I18N::number($this->totalRepositoriesQuery()); 1662 } 1663 1664 /** 1665 * @return string 1666 */ 1667 public function totalSources(): string 1668 { 1669 return I18N::number($this->totalSourcesQuery()); 1670 } 1671 1672 /** 1673 * @return string 1674 */ 1675 public function totalNotes(): string 1676 { 1677 return I18N::number($this->totalNotesQuery()); 1678 } 1679 1680 /** 1681 * @return string 1682 */ 1683 public function totalIndividualsPercentage(): string 1684 { 1685 return $this->getPercentage( 1686 $this->totalIndividualsQuery(), 1687 $this->totalRecordsQuery() 1688 ); 1689 } 1690 1691 /** 1692 * @return string 1693 */ 1694 public function totalIndisWithSourcesPercentage(): string 1695 { 1696 return $this->getPercentage( 1697 $this->totalIndisWithSourcesQuery(), 1698 $this->totalIndividualsQuery() 1699 ); 1700 } 1701 1702 /** 1703 * @return string 1704 */ 1705 public function totalFamiliesPercentage(): string 1706 { 1707 return $this->getPercentage( 1708 $this->totalFamiliesQuery(), 1709 $this->totalRecordsQuery() 1710 ); 1711 } 1712 1713 /** 1714 * @return string 1715 */ 1716 public function totalFamsWithSourcesPercentage(): string 1717 { 1718 return $this->getPercentage( 1719 $this->totalFamsWithSourcesQuery(), 1720 $this->totalFamiliesQuery() 1721 ); 1722 } 1723 1724 /** 1725 * @return string 1726 */ 1727 public function totalRepositoriesPercentage(): string 1728 { 1729 return $this->getPercentage( 1730 $this->totalRepositoriesQuery(), 1731 $this->totalRecordsQuery() 1732 ); 1733 } 1734 1735 /** 1736 * @return string 1737 */ 1738 public function totalSourcesPercentage(): string 1739 { 1740 return $this->getPercentage( 1741 $this->totalSourcesQuery(), 1742 $this->totalRecordsQuery() 1743 ); 1744 } 1745 1746 /** 1747 * @return string 1748 */ 1749 public function totalNotesPercentage(): string 1750 { 1751 return $this->getPercentage( 1752 $this->totalNotesQuery(), 1753 $this->totalRecordsQuery() 1754 ); 1755 } 1756 1757 /** 1758 * @return string 1759 */ 1760 public function totalLivingPercentage(): string 1761 { 1762 return $this->getPercentage( 1763 $this->totalLivingQuery(), 1764 $this->totalIndividualsQuery() 1765 ); 1766 } 1767 1768 /** 1769 * @return string 1770 */ 1771 public function totalDeceasedPercentage(): string 1772 { 1773 return $this->getPercentage( 1774 $this->totalDeceasedQuery(), 1775 $this->totalIndividualsQuery() 1776 ); 1777 } 1778 1779 /** 1780 * @return string 1781 */ 1782 public function totalSexMalesPercentage(): string 1783 { 1784 return $this->getPercentage( 1785 $this->totalSexMalesQuery(), 1786 $this->totalIndividualsQuery() 1787 ); 1788 } 1789 1790 /** 1791 * @return string 1792 */ 1793 public function totalSexFemalesPercentage(): string 1794 { 1795 return $this->getPercentage( 1796 $this->totalSexFemalesQuery(), 1797 $this->totalIndividualsQuery() 1798 ); 1799 } 1800 1801 /** 1802 * @return string 1803 */ 1804 public function totalSexUnknownPercentage(): string 1805 { 1806 return $this->getPercentage( 1807 $this->totalSexUnknownQuery(), 1808 $this->totalIndividualsQuery() 1809 ); 1810 } 1811 1812 /** 1813 * Create a chart of common given names. 1814 * 1815 * @param string|null $color_from 1816 * @param string|null $color_to 1817 * @param int $maxtoshow 1818 * 1819 * @return string 1820 */ 1821 public function chartCommonGiven( 1822 string $color_from = null, 1823 string $color_to = null, 1824 int $maxtoshow = 7 1825 ): string { 1826 $tot_indi = $this->totalIndividualsQuery(); 1827 $given = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow); 1828 1829 if ($given === []) { 1830 return I18N::translate('This information is not available.'); 1831 } 1832 1833 return (new ChartCommonGiven($this->color_service)) 1834 ->chartCommonGiven($tot_indi, $given, $color_from, $color_to); 1835 } 1836 1837 /** 1838 * Create a chart of common surnames. 1839 * 1840 * @param string|null $color_from 1841 * @param string|null $color_to 1842 * @param int $number_of_surnames 1843 * 1844 * @return string 1845 */ 1846 public function chartCommonSurnames( 1847 string $color_from = null, 1848 string $color_to = null, 1849 int $number_of_surnames = 10 1850 ): string { 1851 $tot_indi = $this->totalIndividualsQuery(); 1852 $all_surnames = $this->topSurnames($number_of_surnames, 0); 1853 1854 if ($all_surnames === []) { 1855 return I18N::translate('This information is not available.'); 1856 } 1857 1858 $surname_tradition = Registry::surnameTraditionFactory() 1859 ->make($this->tree->getPreference('SURNAME_TRADITION')); 1860 1861 return (new ChartCommonSurname($this->color_service, $surname_tradition)) 1862 ->chartCommonSurnames($tot_indi, $all_surnames, $color_from, $color_to); 1863 } 1864 1865 /** 1866 * Create a chart showing mortality. 1867 * 1868 * @param string|null $color_living 1869 * @param string|null $color_dead 1870 * 1871 * @return string 1872 */ 1873 public function chartMortality(string $color_living = null, string $color_dead = null): string 1874 { 1875 $tot_l = $this->totalLivingQuery(); 1876 $tot_d = $this->totalDeceasedQuery(); 1877 1878 return (new ChartMortality($this->color_service)) 1879 ->chartMortality($tot_l, $tot_d, $color_living, $color_dead); 1880 } 1881 1882 /** 1883 * Create a chart showing individuals with/without sources. 1884 * 1885 * @param string|null $color_from 1886 * @param string|null $color_to 1887 * 1888 * @return string 1889 */ 1890 public function chartIndisWithSources( 1891 string $color_from = null, 1892 string $color_to = null 1893 ): string { 1894 $tot_indi = $this->totalIndividualsQuery(); 1895 $tot_indi_source = $this->totalIndisWithSourcesQuery(); 1896 1897 return (new ChartIndividualWithSources($this->color_service)) 1898 ->chartIndisWithSources($tot_indi, $tot_indi_source, $color_from, $color_to); 1899 } 1900 1901 /** 1902 * Create a chart of individuals with/without sources. 1903 * 1904 * @param string|null $color_from 1905 * @param string|null $color_to 1906 * 1907 * @return string 1908 */ 1909 public function chartFamsWithSources( 1910 string $color_from = null, 1911 string $color_to = null 1912 ): string { 1913 $tot_fam = $this->totalFamiliesQuery(); 1914 $tot_fam_source = $this->totalFamsWithSourcesQuery(); 1915 1916 return (new ChartFamilyWithSources($this->color_service)) 1917 ->chartFamsWithSources($tot_fam, $tot_fam_source, $color_from, $color_to); 1918 } 1919 1920 /** 1921 * @param string|null $color_female 1922 * @param string|null $color_male 1923 * @param string|null $color_unknown 1924 * 1925 * @return string 1926 */ 1927 public function chartSex( 1928 string $color_female = null, 1929 string $color_male = null, 1930 string $color_unknown = null 1931 ): string { 1932 $tot_m = $this->totalSexMalesQuery(); 1933 $tot_f = $this->totalSexFemalesQuery(); 1934 $tot_u = $this->totalSexUnknownQuery(); 1935 1936 return (new ChartSex()) 1937 ->chartSex($tot_m, $tot_f, $tot_u, $color_female, $color_male, $color_unknown); 1938 } 1939 1940 /** 1941 * Query individuals, with their births and deaths. 1942 * 1943 * @param string $sex 1944 * 1945 * @return Builder 1946 */ 1947 private function birthAndDeathQuery(string $sex): Builder 1948 { 1949 $query = DB::table('individuals') 1950 ->where('i_file', '=', $this->tree->id()) 1951 ->join('dates AS birth', static function (JoinClause $join): void { 1952 $join 1953 ->on('birth.d_file', '=', 'i_file') 1954 ->on('birth.d_gid', '=', 'i_id'); 1955 }) 1956 ->join('dates AS death', static function (JoinClause $join): void { 1957 $join 1958 ->on('death.d_file', '=', 'i_file') 1959 ->on('death.d_gid', '=', 'i_id'); 1960 }) 1961 ->where('birth.d_fact', '=', 'BIRT') 1962 ->where('death.d_fact', '=', 'DEAT') 1963 ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2') 1964 ->where('birth.d_julianday2', '<>', 0); 1965 1966 if ($sex === 'M' || $sex === 'F') { 1967 $query->where('i_sex', '=', $sex); 1968 } 1969 1970 return $query; 1971 } 1972} 1973