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