1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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)) { 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 ->all(); 536 } 537 538 return $surnames; 539 } 540 541 /** 542 * Find common surnames. 543 * 544 * @return string 545 */ 546 public function getCommonSurname(): string 547 { 548 $top_surname = $this->topSurnames(1, 0); 549 550 return implode(', ', array_keys(array_shift($top_surname) ?? [])); 551 } 552 553 /** 554 * Find common surnames. 555 * 556 * @param string $type 557 * @param bool $show_tot 558 * @param int $threshold 559 * @param int $number_of_surnames 560 * @param string $sorting 561 * 562 * @return string 563 */ 564 private function commonSurnamesQuery( 565 string $type, 566 bool $show_tot, 567 int $threshold, 568 int $number_of_surnames, 569 string $sorting 570 ): string { 571 $surnames = $this->topSurnames($number_of_surnames, $threshold); 572 573 switch ($sorting) { 574 default: 575 case 'alpha': 576 uksort($surnames, I18N::comparator()); 577 break; 578 case 'count': 579 break; 580 case 'rcount': 581 $surnames = array_reverse($surnames, true); 582 break; 583 } 584 585 // find a module providing individual lists 586 $module_service = app(ModuleService::class); 587 assert($module_service instanceof ModuleService); 588 589 $module = $module_service 590 ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user()) 591 ->first(static fn (ModuleInterface $module): bool => $module instanceof IndividualListModule); 592 593 if ($type === 'list') { 594 return view('lists/surnames-bullet-list', [ 595 'surnames' => $surnames, 596 'module' => $module, 597 'totals' => $show_tot, 598 'tree' => $this->tree, 599 ]); 600 } 601 602 return view('lists/surnames-compact-list', [ 603 'surnames' => $surnames, 604 'module' => $module, 605 'totals' => $show_tot, 606 'tree' => $this->tree, 607 ]); 608 } 609 610 /** 611 * Find common surnames. 612 * 613 * @param int $threshold 614 * @param int $number_of_surnames 615 * @param string $sorting 616 * 617 * @return string 618 */ 619 public function commonSurnames( 620 int $threshold = 1, 621 int $number_of_surnames = 10, 622 string $sorting = 'alpha' 623 ): string { 624 return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting); 625 } 626 627 /** 628 * Find common surnames. 629 * 630 * @param int $threshold 631 * @param int $number_of_surnames 632 * @param string $sorting 633 * 634 * @return string 635 */ 636 public function commonSurnamesTotals( 637 int $threshold = 1, 638 int $number_of_surnames = 10, 639 string $sorting = 'count' 640 ): string { 641 return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting); 642 } 643 644 /** 645 * Find common surnames. 646 * 647 * @param int $threshold 648 * @param int $number_of_surnames 649 * @param string $sorting 650 * 651 * @return string 652 */ 653 public function commonSurnamesList( 654 int $threshold = 1, 655 int $number_of_surnames = 10, 656 string $sorting = 'alpha' 657 ): string { 658 return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting); 659 } 660 661 /** 662 * Find common surnames. 663 * 664 * @param int $threshold 665 * @param int $number_of_surnames 666 * @param string $sorting 667 * 668 * @return string 669 */ 670 public function commonSurnamesListTotals( 671 int $threshold = 1, 672 int $number_of_surnames = 10, 673 string $sorting = 'count' 674 ): string { 675 return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting); 676 } 677 678 /** 679 * Get a count of births by month. 680 * 681 * @param int $year1 682 * @param int $year2 683 * 684 * @return Builder 685 */ 686 public function statsBirthQuery(int $year1 = -1, int $year2 = -1): Builder 687 { 688 $query = DB::table('dates') 689 ->select(['d_month', new Expression('COUNT(*) AS total')]) 690 ->where('d_file', '=', $this->tree->id()) 691 ->where('d_fact', '=', 'BIRT') 692 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 693 ->groupBy(['d_month']); 694 695 if ($year1 >= 0 && $year2 >= 0) { 696 $query->whereBetween('d_year', [$year1, $year2]); 697 } 698 699 return $query; 700 } 701 702 /** 703 * Get a count of births by month. 704 * 705 * @param int $year1 706 * @param int $year2 707 * 708 * @return Builder 709 */ 710 public function statsBirthBySexQuery(int $year1 = -1, int $year2 = -1): Builder 711 { 712 return $this->statsBirthQuery($year1, $year2) 713 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 714 ->join('individuals', static function (JoinClause $join): void { 715 $join 716 ->on('i_id', '=', 'd_gid') 717 ->on('i_file', '=', 'd_file'); 718 }) 719 ->groupBy(['i_sex']); 720 } 721 722 /** 723 * General query on births. 724 * 725 * @param string|null $color_from 726 * @param string|null $color_to 727 * 728 * @return string 729 */ 730 public function statsBirth(string $color_from = null, string $color_to = null): string 731 { 732 return (new ChartBirth($this->century_service, $this->color_service, $this->tree)) 733 ->chartBirth($color_from, $color_to); 734 } 735 736 /** 737 * Get a list of death dates. 738 * 739 * @param int $year1 740 * @param int $year2 741 * 742 * @return Builder 743 */ 744 public function statsDeathQuery(int $year1 = -1, int $year2 = -1): Builder 745 { 746 $query = DB::table('dates') 747 ->select(['d_month', new Expression('COUNT(*) AS total')]) 748 ->where('d_file', '=', $this->tree->id()) 749 ->where('d_fact', '=', 'DEAT') 750 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 751 ->groupBy(['d_month']); 752 753 if ($year1 >= 0 && $year2 >= 0) { 754 $query->whereBetween('d_year', [$year1, $year2]); 755 } 756 757 return $query; 758 } 759 760 /** 761 * Get a list of death dates. 762 * 763 * @param int $year1 764 * @param int $year2 765 * 766 * @return Builder 767 */ 768 public function statsDeathBySexQuery(int $year1 = -1, int $year2 = -1): Builder 769 { 770 return $this->statsDeathQuery($year1, $year2) 771 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 772 ->join('individuals', static function (JoinClause $join): void { 773 $join 774 ->on('i_id', '=', 'd_gid') 775 ->on('i_file', '=', 'd_file'); 776 }) 777 ->groupBy(['i_sex']); 778 } 779 780 /** 781 * General query on deaths. 782 * 783 * @param string|null $color_from 784 * @param string|null $color_to 785 * 786 * @return string 787 */ 788 public function statsDeath(string $color_from = null, string $color_to = null): string 789 { 790 return (new ChartDeath($this->century_service, $this->color_service, $this->tree)) 791 ->chartDeath($color_from, $color_to); 792 } 793 794 /** 795 * General query on ages. 796 * 797 * @param string $related 798 * @param string $sex 799 * @param int $year1 800 * @param int $year2 801 * 802 * @return array<stdClass> 803 */ 804 public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array 805 { 806 $prefix = DB::connection()->getTablePrefix(); 807 808 $query = $this->birthAndDeathQuery($sex); 809 810 if ($year1 >= 0 && $year2 >= 0) { 811 $query 812 ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 813 ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']); 814 815 if ($related === 'BIRT') { 816 $query->whereBetween('birth.d_year', [$year1, $year2]); 817 } elseif ($related === 'DEAT') { 818 $query->whereBetween('death.d_year', [$year1, $year2]); 819 } 820 } 821 822 return $query 823 ->select(new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')) 824 ->orderBy('days', 'desc') 825 ->get() 826 ->all(); 827 } 828 829 /** 830 * General query on ages. 831 * 832 * @return string 833 */ 834 public function statsAge(): string 835 { 836 return (new ChartAge($this->century_service, $this->tree))->chartAge(); 837 } 838 839 /** 840 * Lifespan 841 * 842 * @param string $type 843 * @param string $sex 844 * 845 * @return string 846 */ 847 private function longlifeQuery(string $type, string $sex): string 848 { 849 $prefix = DB::connection()->getTablePrefix(); 850 851 $row = $this->birthAndDeathQuery($sex) 852 ->orderBy('days', 'desc') 853 ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')]) 854 ->first(); 855 856 if ($row === null) { 857 return ''; 858 } 859 860 /** @var Individual $individual */ 861 $individual = Registry::individualFactory()->mapper($this->tree)($row); 862 863 if ($type !== 'age' && !$individual->canShow()) { 864 return I18N::translate('This information is private and cannot be shown.'); 865 } 866 867 switch ($type) { 868 default: 869 case 'full': 870 return $individual->formatList(); 871 872 case 'age': 873 return I18N::number((int) ($row->days / 365.25)); 874 875 case 'name': 876 return '<a href="' . e($individual->url()) . '">' . $individual->fullName() . '</a>'; 877 } 878 } 879 880 /** 881 * Find the longest lived individual. 882 * 883 * @return string 884 */ 885 public function longestLife(): string 886 { 887 return $this->longlifeQuery('full', 'BOTH'); 888 } 889 890 /** 891 * Find the age of the longest lived individual. 892 * 893 * @return string 894 */ 895 public function longestLifeAge(): string 896 { 897 return $this->longlifeQuery('age', 'BOTH'); 898 } 899 900 /** 901 * Find the name of the longest lived individual. 902 * 903 * @return string 904 */ 905 public function longestLifeName(): string 906 { 907 return $this->longlifeQuery('name', 'BOTH'); 908 } 909 910 /** 911 * Find the longest lived female. 912 * 913 * @return string 914 */ 915 public function longestLifeFemale(): string 916 { 917 return $this->longlifeQuery('full', 'F'); 918 } 919 920 /** 921 * Find the age of the longest lived female. 922 * 923 * @return string 924 */ 925 public function longestLifeFemaleAge(): string 926 { 927 return $this->longlifeQuery('age', 'F'); 928 } 929 930 /** 931 * Find the name of the longest lived female. 932 * 933 * @return string 934 */ 935 public function longestLifeFemaleName(): string 936 { 937 return $this->longlifeQuery('name', 'F'); 938 } 939 940 /** 941 * Find the longest lived male. 942 * 943 * @return string 944 */ 945 public function longestLifeMale(): string 946 { 947 return $this->longlifeQuery('full', 'M'); 948 } 949 950 /** 951 * Find the age of the longest lived male. 952 * 953 * @return string 954 */ 955 public function longestLifeMaleAge(): string 956 { 957 return $this->longlifeQuery('age', 'M'); 958 } 959 960 /** 961 * Find the name of the longest lived male. 962 * 963 * @return string 964 */ 965 public function longestLifeMaleName(): string 966 { 967 return $this->longlifeQuery('name', 'M'); 968 } 969 970 /** 971 * Returns the calculated age the time of event. 972 * 973 * @param int $days The age from the database record 974 * 975 * @return string 976 */ 977 private function calculateAge(int $days): string 978 { 979 if ($days < 31) { 980 return I18N::plural('%s day', '%s days', $days, I18N::number($days)); 981 } 982 983 if ($days < 365) { 984 $months = (int) ($days / 30.5); 985 return I18N::plural('%s month', '%s months', $months, I18N::number($months)); 986 } 987 988 $years = (int) ($days / 365.25); 989 990 return I18N::plural('%s year', '%s years', $years, I18N::number($years)); 991 } 992 993 /** 994 * Find the oldest individuals. 995 * 996 * @param string $sex 997 * @param int $total 998 * 999 * @return array<array<string,mixed>> 1000 */ 1001 private function topTenOldestQuery(string $sex, int $total): array 1002 { 1003 $prefix = DB::connection()->getTablePrefix(); 1004 1005 $rows = $this->birthAndDeathQuery($sex) 1006 ->groupBy(['i_id', 'i_file']) 1007 ->orderBy('days', 'desc') 1008 ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')]) 1009 ->take($total) 1010 ->get(); 1011 1012 $top10 = []; 1013 foreach ($rows as $row) { 1014 /** @var Individual $individual */ 1015 $individual = Registry::individualFactory()->mapper($this->tree)($row); 1016 1017 if ($individual->canShow()) { 1018 $top10[] = [ 1019 'person' => $individual, 1020 'age' => $this->calculateAge((int) $row->days), 1021 ]; 1022 } 1023 } 1024 1025 return $top10; 1026 } 1027 1028 /** 1029 * Find the oldest individuals. 1030 * 1031 * @param int $total 1032 * 1033 * @return string 1034 */ 1035 public function topTenOldest(int $total = 10): string 1036 { 1037 $records = $this->topTenOldestQuery('BOTH', $total); 1038 1039 return view('statistics/individuals/top10-nolist', [ 1040 'records' => $records, 1041 ]); 1042 } 1043 1044 /** 1045 * Find the oldest living individuals. 1046 * 1047 * @param int $total 1048 * 1049 * @return string 1050 */ 1051 public function topTenOldestList(int $total = 10): string 1052 { 1053 $records = $this->topTenOldestQuery('BOTH', $total); 1054 1055 return view('statistics/individuals/top10-list', [ 1056 'records' => $records, 1057 ]); 1058 } 1059 1060 /** 1061 * Find the oldest females. 1062 * 1063 * @param int $total 1064 * 1065 * @return string 1066 */ 1067 public function topTenOldestFemale(int $total = 10): string 1068 { 1069 $records = $this->topTenOldestQuery('F', $total); 1070 1071 return view('statistics/individuals/top10-nolist', [ 1072 'records' => $records, 1073 ]); 1074 } 1075 1076 /** 1077 * Find the oldest living females. 1078 * 1079 * @param int $total 1080 * 1081 * @return string 1082 */ 1083 public function topTenOldestFemaleList(int $total = 10): string 1084 { 1085 $records = $this->topTenOldestQuery('F', $total); 1086 1087 return view('statistics/individuals/top10-list', [ 1088 'records' => $records, 1089 ]); 1090 } 1091 1092 /** 1093 * Find the longest lived males. 1094 * 1095 * @param int $total 1096 * 1097 * @return string 1098 */ 1099 public function topTenOldestMale(int $total = 10): string 1100 { 1101 $records = $this->topTenOldestQuery('M', $total); 1102 1103 return view('statistics/individuals/top10-nolist', [ 1104 'records' => $records, 1105 ]); 1106 } 1107 1108 /** 1109 * Find the longest lived males. 1110 * 1111 * @param int $total 1112 * 1113 * @return string 1114 */ 1115 public function topTenOldestMaleList(int $total = 10): string 1116 { 1117 $records = $this->topTenOldestQuery('M', $total); 1118 1119 return view('statistics/individuals/top10-list', [ 1120 'records' => $records, 1121 ]); 1122 } 1123 1124 /** 1125 * Find the oldest living individuals. 1126 * 1127 * @param string $sex "M", "F" or "BOTH" 1128 * @param int $total 1129 * 1130 * @return array<array<string,mixed>> 1131 */ 1132 private function topTenOldestAliveQuery(string $sex, int $total): array 1133 { 1134 $query = DB::table('dates') 1135 ->join('individuals', static function (JoinClause $join): void { 1136 $join 1137 ->on('i_id', '=', 'd_gid') 1138 ->on('i_file', '=', 'd_file'); 1139 }) 1140 ->where('d_file', '=', $this->tree->id()) 1141 ->where('d_julianday1', '<>', 0) 1142 ->where('d_fact', '=', 'BIRT') 1143 ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%") 1144 ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%") 1145 ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%"); 1146 1147 if ($sex === 'F' || $sex === 'M') { 1148 $query->where('i_sex', '=', $sex); 1149 } 1150 1151 return $query 1152 ->groupBy(['i_id', 'i_file']) 1153 ->orderBy(new Expression('MIN(d_julianday1)')) 1154 ->select(['individuals.*']) 1155 ->take($total) 1156 ->get() 1157 ->map(Registry::individualFactory()->mapper($this->tree)) 1158 ->filter(GedcomRecord::accessFilter()) 1159 ->map(function (Individual $individual): array { 1160 return [ 1161 'person' => $individual, 1162 'age' => $this->calculateAge(Registry::timestampFactory()->now()->julianDay() - $individual->getBirthDate()->minimumJulianDay()), 1163 ]; 1164 }) 1165 ->all(); 1166 } 1167 1168 /** 1169 * Find the oldest living individuals. 1170 * 1171 * @param int $total 1172 * 1173 * @return string 1174 */ 1175 public function topTenOldestAlive(int $total = 10): string 1176 { 1177 if (!Auth::isMember($this->tree)) { 1178 return I18N::translate('This information is private and cannot be shown.'); 1179 } 1180 1181 $records = $this->topTenOldestAliveQuery('BOTH', $total); 1182 1183 return view('statistics/individuals/top10-nolist', [ 1184 'records' => $records, 1185 ]); 1186 } 1187 1188 /** 1189 * Find the oldest living individuals. 1190 * 1191 * @param int $total 1192 * 1193 * @return string 1194 */ 1195 public function topTenOldestListAlive(int $total = 10): string 1196 { 1197 if (!Auth::isMember($this->tree)) { 1198 return I18N::translate('This information is private and cannot be shown.'); 1199 } 1200 1201 $records = $this->topTenOldestAliveQuery('BOTH', $total); 1202 1203 return view('statistics/individuals/top10-list', [ 1204 'records' => $records, 1205 ]); 1206 } 1207 1208 /** 1209 * Find the oldest living females. 1210 * 1211 * @param int $total 1212 * 1213 * @return string 1214 */ 1215 public function topTenOldestFemaleAlive(int $total = 10): string 1216 { 1217 if (!Auth::isMember($this->tree)) { 1218 return I18N::translate('This information is private and cannot be shown.'); 1219 } 1220 1221 $records = $this->topTenOldestAliveQuery('F', $total); 1222 1223 return view('statistics/individuals/top10-nolist', [ 1224 'records' => $records, 1225 ]); 1226 } 1227 1228 /** 1229 * Find the oldest living females. 1230 * 1231 * @param int $total 1232 * 1233 * @return string 1234 */ 1235 public function topTenOldestFemaleListAlive(int $total = 10): string 1236 { 1237 if (!Auth::isMember($this->tree)) { 1238 return I18N::translate('This information is private and cannot be shown.'); 1239 } 1240 1241 $records = $this->topTenOldestAliveQuery('F', $total); 1242 1243 return view('statistics/individuals/top10-list', [ 1244 'records' => $records, 1245 ]); 1246 } 1247 1248 /** 1249 * Find the longest lived living males. 1250 * 1251 * @param int $total 1252 * 1253 * @return string 1254 */ 1255 public function topTenOldestMaleAlive(int $total = 10): string 1256 { 1257 if (!Auth::isMember($this->tree)) { 1258 return I18N::translate('This information is private and cannot be shown.'); 1259 } 1260 1261 $records = $this->topTenOldestAliveQuery('M', $total); 1262 1263 return view('statistics/individuals/top10-nolist', [ 1264 'records' => $records, 1265 ]); 1266 } 1267 1268 /** 1269 * Find the longest lived living males. 1270 * 1271 * @param int $total 1272 * 1273 * @return string 1274 */ 1275 public function topTenOldestMaleListAlive(int $total = 10): string 1276 { 1277 if (!Auth::isMember($this->tree)) { 1278 return I18N::translate('This information is private and cannot be shown.'); 1279 } 1280 1281 $records = $this->topTenOldestAliveQuery('M', $total); 1282 1283 return view('statistics/individuals/top10-list', [ 1284 'records' => $records, 1285 ]); 1286 } 1287 1288 /** 1289 * Find the average lifespan. 1290 * 1291 * @param string $sex "M", "F" or "BOTH" 1292 * @param bool $show_years 1293 * 1294 * @return string 1295 */ 1296 private function averageLifespanQuery(string $sex, bool $show_years): string 1297 { 1298 $prefix = DB::connection()->getTablePrefix(); 1299 1300 $days = (int) $this->birthAndDeathQuery($sex) 1301 ->select(new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')) 1302 ->value('days'); 1303 1304 if ($show_years) { 1305 return $this->calculateAge($days); 1306 } 1307 1308 return I18N::number((int) ($days / 365.25)); 1309 } 1310 1311 /** 1312 * Find the average lifespan. 1313 * 1314 * @param bool $show_years 1315 * 1316 * @return string 1317 */ 1318 public function averageLifespan(bool $show_years): string 1319 { 1320 return $this->averageLifespanQuery('BOTH', $show_years); 1321 } 1322 1323 /** 1324 * Find the average lifespan of females. 1325 * 1326 * @param bool $show_years 1327 * 1328 * @return string 1329 */ 1330 public function averageLifespanFemale(bool $show_years): string 1331 { 1332 return $this->averageLifespanQuery('F', $show_years); 1333 } 1334 1335 /** 1336 * Find the average male lifespan. 1337 * 1338 * @param bool $show_years 1339 * 1340 * @return string 1341 */ 1342 public function averageLifespanMale(bool $show_years): string 1343 { 1344 return $this->averageLifespanQuery('M', $show_years); 1345 } 1346 1347 /** 1348 * Convert totals into percentages. 1349 * 1350 * @param int $count 1351 * @param int $total 1352 * 1353 * @return string 1354 */ 1355 private function getPercentage(int $count, int $total): string 1356 { 1357 return $total !== 0 ? I18N::percentage($count / $total, 1) : ''; 1358 } 1359 1360 /** 1361 * Returns how many individuals exist in the tree. 1362 * 1363 * @return int 1364 */ 1365 private function totalIndividualsQuery(): int 1366 { 1367 return DB::table('individuals') 1368 ->where('i_file', '=', $this->tree->id()) 1369 ->count(); 1370 } 1371 1372 /** 1373 * Count the number of living individuals. 1374 * 1375 * The totalLiving/totalDeceased queries assume that every dead person will 1376 * have a DEAT record. It will not include individuals who were born more 1377 * than MAX_ALIVE_AGE years ago, and who have no DEAT record. 1378 * A good reason to run the “Add missing DEAT records” batch-update! 1379 * 1380 * @return int 1381 */ 1382 private function totalLivingQuery(): int 1383 { 1384 $query = DB::table('individuals') 1385 ->where('i_file', '=', $this->tree->id()); 1386 1387 foreach (Gedcom::DEATH_EVENTS as $death_event) { 1388 $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%'); 1389 } 1390 1391 return $query->count(); 1392 } 1393 1394 /** 1395 * Count the number of dead individuals. 1396 * 1397 * @return int 1398 */ 1399 private function totalDeceasedQuery(): int 1400 { 1401 return DB::table('individuals') 1402 ->where('i_file', '=', $this->tree->id()) 1403 ->where(static function (Builder $query): void { 1404 foreach (Gedcom::DEATH_EVENTS as $death_event) { 1405 $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%'); 1406 } 1407 }) 1408 ->count(); 1409 } 1410 1411 /** 1412 * Returns the total count of a specific sex. 1413 * 1414 * @param string $sex The sex to query 1415 * 1416 * @return int 1417 */ 1418 private function getTotalSexQuery(string $sex): int 1419 { 1420 return DB::table('individuals') 1421 ->where('i_file', '=', $this->tree->id()) 1422 ->where('i_sex', '=', $sex) 1423 ->count(); 1424 } 1425 1426 /** 1427 * Returns the total number of males. 1428 * 1429 * @return int 1430 */ 1431 private function totalSexMalesQuery(): int 1432 { 1433 return $this->getTotalSexQuery('M'); 1434 } 1435 1436 /** 1437 * Returns the total number of females. 1438 * 1439 * @return int 1440 */ 1441 private function totalSexFemalesQuery(): int 1442 { 1443 return $this->getTotalSexQuery('F'); 1444 } 1445 1446 /** 1447 * Returns the total number of individuals with unknown sex. 1448 * 1449 * @return int 1450 */ 1451 private function totalSexUnknownQuery(): int 1452 { 1453 return $this->getTotalSexQuery('U'); 1454 } 1455 1456 /** 1457 * Count the total families. 1458 * 1459 * @return int 1460 */ 1461 private function totalFamiliesQuery(): int 1462 { 1463 return DB::table('families') 1464 ->where('f_file', '=', $this->tree->id()) 1465 ->count(); 1466 } 1467 1468 /** 1469 * How many individuals have one or more sources. 1470 * 1471 * @return int 1472 */ 1473 private function totalIndisWithSourcesQuery(): int 1474 { 1475 return DB::table('individuals') 1476 ->select(['i_id']) 1477 ->distinct() 1478 ->join('link', static function (JoinClause $join): void { 1479 $join->on('i_id', '=', 'l_from') 1480 ->on('i_file', '=', 'l_file'); 1481 }) 1482 ->where('l_file', '=', $this->tree->id()) 1483 ->where('l_type', '=', 'SOUR') 1484 ->count('i_id'); 1485 } 1486 1487 /** 1488 * Count the families with source records. 1489 * 1490 * @return int 1491 */ 1492 private function totalFamsWithSourcesQuery(): int 1493 { 1494 return DB::table('families') 1495 ->select(['f_id']) 1496 ->distinct() 1497 ->join('link', static function (JoinClause $join): void { 1498 $join->on('f_id', '=', 'l_from') 1499 ->on('f_file', '=', 'l_file'); 1500 }) 1501 ->where('l_file', '=', $this->tree->id()) 1502 ->where('l_type', '=', 'SOUR') 1503 ->count('f_id'); 1504 } 1505 1506 /** 1507 * Count the number of repositories. 1508 * 1509 * @return int 1510 */ 1511 private function totalRepositoriesQuery(): int 1512 { 1513 return DB::table('other') 1514 ->where('o_file', '=', $this->tree->id()) 1515 ->where('o_type', '=', 'REPO') 1516 ->count(); 1517 } 1518 1519 /** 1520 * Count the total number of sources. 1521 * 1522 * @return int 1523 */ 1524 private function totalSourcesQuery(): int 1525 { 1526 return DB::table('sources') 1527 ->where('s_file', '=', $this->tree->id()) 1528 ->count(); 1529 } 1530 1531 /** 1532 * Count the number of notes. 1533 * 1534 * @return int 1535 */ 1536 private function totalNotesQuery(): int 1537 { 1538 return DB::table('other') 1539 ->where('o_file', '=', $this->tree->id()) 1540 ->where('o_type', '=', 'NOTE') 1541 ->count(); 1542 } 1543 1544 /** 1545 * Returns the total number of records. 1546 * 1547 * @return int 1548 */ 1549 private function totalRecordsQuery(): int 1550 { 1551 return $this->totalIndividualsQuery() 1552 + $this->totalFamiliesQuery() 1553 + $this->totalNotesQuery() 1554 + $this->totalRepositoriesQuery() 1555 + $this->totalSourcesQuery(); 1556 } 1557 1558 /** 1559 * @return string 1560 */ 1561 public function totalRecords(): string 1562 { 1563 return I18N::number($this->totalRecordsQuery()); 1564 } 1565 1566 /** 1567 * @return string 1568 */ 1569 public function totalIndividuals(): string 1570 { 1571 return I18N::number($this->totalIndividualsQuery()); 1572 } 1573 1574 /** 1575 * Count the number of living individuals. 1576 * 1577 * @return string 1578 */ 1579 public function totalLiving(): string 1580 { 1581 return I18N::number($this->totalLivingQuery()); 1582 } 1583 1584 /** 1585 * Count the number of dead individuals. 1586 * 1587 * @return string 1588 */ 1589 public function totalDeceased(): string 1590 { 1591 return I18N::number($this->totalDeceasedQuery()); 1592 } 1593 1594 /** 1595 * @return string 1596 */ 1597 public function totalSexMales(): string 1598 { 1599 return I18N::number($this->totalSexMalesQuery()); 1600 } 1601 1602 /** 1603 * @return string 1604 */ 1605 public function totalSexFemales(): string 1606 { 1607 return I18N::number($this->totalSexFemalesQuery()); 1608 } 1609 1610 /** 1611 * @return string 1612 */ 1613 public function totalSexUnknown(): string 1614 { 1615 return I18N::number($this->totalSexUnknownQuery()); 1616 } 1617 1618 /** 1619 * @return string 1620 */ 1621 public function totalFamilies(): string 1622 { 1623 return I18N::number($this->totalFamiliesQuery()); 1624 } 1625 1626 /** 1627 * How many individuals have one or more sources. 1628 * 1629 * @return string 1630 */ 1631 public function totalIndisWithSources(): string 1632 { 1633 return I18N::number($this->totalIndisWithSourcesQuery()); 1634 } 1635 1636 /** 1637 * Count the families with with source records. 1638 * 1639 * @return string 1640 */ 1641 public function totalFamsWithSources(): string 1642 { 1643 return I18N::number($this->totalFamsWithSourcesQuery()); 1644 } 1645 1646 /** 1647 * @return string 1648 */ 1649 public function totalRepositories(): string 1650 { 1651 return I18N::number($this->totalRepositoriesQuery()); 1652 } 1653 1654 /** 1655 * @return string 1656 */ 1657 public function totalSources(): string 1658 { 1659 return I18N::number($this->totalSourcesQuery()); 1660 } 1661 1662 /** 1663 * @return string 1664 */ 1665 public function totalNotes(): string 1666 { 1667 return I18N::number($this->totalNotesQuery()); 1668 } 1669 1670 /** 1671 * @return string 1672 */ 1673 public function totalIndividualsPercentage(): string 1674 { 1675 return $this->getPercentage( 1676 $this->totalIndividualsQuery(), 1677 $this->totalRecordsQuery() 1678 ); 1679 } 1680 1681 /** 1682 * @return string 1683 */ 1684 public function totalFamiliesPercentage(): string 1685 { 1686 return $this->getPercentage( 1687 $this->totalFamiliesQuery(), 1688 $this->totalRecordsQuery() 1689 ); 1690 } 1691 1692 /** 1693 * @return string 1694 */ 1695 public function totalRepositoriesPercentage(): string 1696 { 1697 return $this->getPercentage( 1698 $this->totalRepositoriesQuery(), 1699 $this->totalRecordsQuery() 1700 ); 1701 } 1702 1703 /** 1704 * @return string 1705 */ 1706 public function totalSourcesPercentage(): string 1707 { 1708 return $this->getPercentage( 1709 $this->totalSourcesQuery(), 1710 $this->totalRecordsQuery() 1711 ); 1712 } 1713 1714 /** 1715 * @return string 1716 */ 1717 public function totalNotesPercentage(): string 1718 { 1719 return $this->getPercentage( 1720 $this->totalNotesQuery(), 1721 $this->totalRecordsQuery() 1722 ); 1723 } 1724 1725 /** 1726 * @return string 1727 */ 1728 public function totalLivingPercentage(): string 1729 { 1730 return $this->getPercentage( 1731 $this->totalLivingQuery(), 1732 $this->totalIndividualsQuery() 1733 ); 1734 } 1735 1736 /** 1737 * @return string 1738 */ 1739 public function totalDeceasedPercentage(): string 1740 { 1741 return $this->getPercentage( 1742 $this->totalDeceasedQuery(), 1743 $this->totalIndividualsQuery() 1744 ); 1745 } 1746 1747 /** 1748 * @return string 1749 */ 1750 public function totalSexMalesPercentage(): string 1751 { 1752 return $this->getPercentage( 1753 $this->totalSexMalesQuery(), 1754 $this->totalIndividualsQuery() 1755 ); 1756 } 1757 1758 /** 1759 * @return string 1760 */ 1761 public function totalSexFemalesPercentage(): string 1762 { 1763 return $this->getPercentage( 1764 $this->totalSexFemalesQuery(), 1765 $this->totalIndividualsQuery() 1766 ); 1767 } 1768 1769 /** 1770 * @return string 1771 */ 1772 public function totalSexUnknownPercentage(): string 1773 { 1774 return $this->getPercentage( 1775 $this->totalSexUnknownQuery(), 1776 $this->totalIndividualsQuery() 1777 ); 1778 } 1779 1780 /** 1781 * Create a chart of common given names. 1782 * 1783 * @param string|null $color_from 1784 * @param string|null $color_to 1785 * @param int $maxtoshow 1786 * 1787 * @return string 1788 */ 1789 public function chartCommonGiven( 1790 string $color_from = null, 1791 string $color_to = null, 1792 int $maxtoshow = 7 1793 ): string { 1794 $tot_indi = $this->totalIndividualsQuery(); 1795 $given = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow); 1796 1797 if ($given === []) { 1798 return I18N::translate('This information is not available.'); 1799 } 1800 1801 return (new ChartCommonGiven($this->color_service)) 1802 ->chartCommonGiven($tot_indi, $given, $color_from, $color_to); 1803 } 1804 1805 /** 1806 * Create a chart of common surnames. 1807 * 1808 * @param string|null $color_from 1809 * @param string|null $color_to 1810 * @param int $number_of_surnames 1811 * 1812 * @return string 1813 */ 1814 public function chartCommonSurnames( 1815 string $color_from = null, 1816 string $color_to = null, 1817 int $number_of_surnames = 10 1818 ): string { 1819 $tot_indi = $this->totalIndividualsQuery(); 1820 $all_surnames = $this->topSurnames($number_of_surnames, 0); 1821 1822 if ($all_surnames === []) { 1823 return I18N::translate('This information is not available.'); 1824 } 1825 1826 $surname_tradition = Registry::surnameTraditionFactory() 1827 ->make($this->tree->getPreference('SURNAME_TRADITION')); 1828 1829 return (new ChartCommonSurname($this->color_service, $surname_tradition)) 1830 ->chartCommonSurnames($tot_indi, $all_surnames, $color_from, $color_to); 1831 } 1832 1833 /** 1834 * Create a chart showing mortality. 1835 * 1836 * @param string|null $color_living 1837 * @param string|null $color_dead 1838 * 1839 * @return string 1840 */ 1841 public function chartMortality(string $color_living = null, string $color_dead = null): string 1842 { 1843 $tot_l = $this->totalLivingQuery(); 1844 $tot_d = $this->totalDeceasedQuery(); 1845 1846 return (new ChartMortality($this->color_service)) 1847 ->chartMortality($tot_l, $tot_d, $color_living, $color_dead); 1848 } 1849 1850 /** 1851 * Create a chart showing individuals with/without sources. 1852 * 1853 * @param string|null $color_from 1854 * @param string|null $color_to 1855 * 1856 * @return string 1857 */ 1858 public function chartIndisWithSources( 1859 string $color_from = null, 1860 string $color_to = null 1861 ): string { 1862 $tot_indi = $this->totalIndividualsQuery(); 1863 $tot_indi_source = $this->totalIndisWithSourcesQuery(); 1864 1865 return (new ChartIndividualWithSources($this->color_service)) 1866 ->chartIndisWithSources($tot_indi, $tot_indi_source, $color_from, $color_to); 1867 } 1868 1869 /** 1870 * Create a chart of individuals with/without sources. 1871 * 1872 * @param string|null $color_from 1873 * @param string|null $color_to 1874 * 1875 * @return string 1876 */ 1877 public function chartFamsWithSources( 1878 string $color_from = null, 1879 string $color_to = null 1880 ): string { 1881 $tot_fam = $this->totalFamiliesQuery(); 1882 $tot_fam_source = $this->totalFamsWithSourcesQuery(); 1883 1884 return (new ChartFamilyWithSources($this->color_service)) 1885 ->chartFamsWithSources($tot_fam, $tot_fam_source, $color_from, $color_to); 1886 } 1887 1888 /** 1889 * @param string|null $color_female 1890 * @param string|null $color_male 1891 * @param string|null $color_unknown 1892 * 1893 * @return string 1894 */ 1895 public function chartSex( 1896 string $color_female = null, 1897 string $color_male = null, 1898 string $color_unknown = null 1899 ): string { 1900 $tot_m = $this->totalSexMalesQuery(); 1901 $tot_f = $this->totalSexFemalesQuery(); 1902 $tot_u = $this->totalSexUnknownQuery(); 1903 1904 return (new ChartSex()) 1905 ->chartSex($tot_m, $tot_f, $tot_u, $color_female, $color_male, $color_unknown); 1906 } 1907 1908 /** 1909 * Query individuals, with their births and deaths. 1910 * 1911 * @param string $sex 1912 * 1913 * @return Builder 1914 */ 1915 private function birthAndDeathQuery(string $sex): Builder 1916 { 1917 $query = DB::table('individuals') 1918 ->where('i_file', '=', $this->tree->id()) 1919 ->join('dates AS birth', static function (JoinClause $join): void { 1920 $join 1921 ->on('birth.d_file', '=', 'i_file') 1922 ->on('birth.d_gid', '=', 'i_id'); 1923 }) 1924 ->join('dates AS death', static function (JoinClause $join): void { 1925 $join 1926 ->on('death.d_file', '=', 'i_file') 1927 ->on('death.d_gid', '=', 'i_id'); 1928 }) 1929 ->where('birth.d_fact', '=', 'BIRT') 1930 ->where('death.d_fact', '=', 'DEAT') 1931 ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2') 1932 ->where('birth.d_julianday2', '<>', 0); 1933 1934 if ($sex === 'M' || $sex === 'F') { 1935 $query->where('i_sex', '=', $sex); 1936 } 1937 1938 return $query; 1939 } 1940} 1941