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