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