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