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