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