1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Statistics\Repository; 20 21use Exception; 22use Fisharebest\Webtrees\Family; 23use Fisharebest\Webtrees\Functions\FunctionsDate; 24use Fisharebest\Webtrees\GedcomRecord; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Statistics\Google\ChartChildren; 28use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; 29use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; 30use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; 31use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; 32use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; 33use Fisharebest\Webtrees\Tree; 34use Illuminate\Database\Capsule\Manager as DB; 35use Illuminate\Database\Query\Builder; 36use Illuminate\Database\Query\Expression; 37use Illuminate\Database\Query\JoinClause; 38use stdClass; 39 40use function in_array; 41 42/** 43 * 44 */ 45class FamilyRepository 46{ 47 /** 48 * @var Tree 49 */ 50 private $tree; 51 52 /** 53 * Constructor. 54 * 55 * @param Tree $tree 56 */ 57 public function __construct(Tree $tree) 58 { 59 $this->tree = $tree; 60 } 61 62 /** 63 * General query on family. 64 * 65 * @param string $type 66 * 67 * @return string 68 */ 69 private function familyQuery(string $type): string 70 { 71 $row = DB::table('families') 72 ->where('f_file', '=', $this->tree->id()) 73 ->orderBy('f_numchil', 'desc') 74 ->first(); 75 76 if ($row === null) { 77 return ''; 78 } 79 80 /** @var Family $family */ 81 $family = Family::rowMapper()($row); 82 83 if (!$family->canShow()) { 84 return I18N::translate('This information is private and cannot be shown.'); 85 } 86 87 switch ($type) { 88 default: 89 case 'full': 90 return $family->formatList(); 91 92 case 'size': 93 return I18N::number((int) $row->f_numchil); 94 95 case 'name': 96 return '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 97 } 98 } 99 100 /** 101 * Find the family with the most children. 102 * 103 * @return string 104 */ 105 public function largestFamily(): string 106 { 107 return $this->familyQuery('full'); 108 } 109 110 /** 111 * Find the number of children in the largest family. 112 * 113 * @return string 114 */ 115 public function largestFamilySize(): string 116 { 117 return $this->familyQuery('size'); 118 } 119 120 /** 121 * Find the family with the most children. 122 * 123 * @return string 124 */ 125 public function largestFamilyName(): string 126 { 127 return $this->familyQuery('name'); 128 } 129 130 /** 131 * Find the couple with the most grandchildren. 132 * 133 * @param int $total 134 * 135 * @return array 136 */ 137 private function topTenGrandFamilyQuery(int $total): array 138 { 139 return DB::table('families') 140 ->join('link AS children', static function (JoinClause $join): void { 141 $join 142 ->on('children.l_from', '=', 'f_id') 143 ->on('children.l_file', '=', 'f_file') 144 ->where('children.l_type', '=', 'CHIL'); 145 })->join('link AS mchildren', static function (JoinClause $join): void { 146 $join 147 ->on('mchildren.l_file', '=', 'children.l_file') 148 ->on('mchildren.l_from', '=', 'children.l_to') 149 ->where('mchildren.l_type', '=', 'FAMS'); 150 })->join('link AS gchildren', static function (JoinClause $join): void { 151 $join 152 ->on('gchildren.l_file', '=', 'mchildren.l_file') 153 ->on('gchildren.l_from', '=', 'mchildren.l_to') 154 ->where('gchildren.l_type', '=', 'CHIL'); 155 }) 156 ->where('f_file', '=', $this->tree->id()) 157 ->groupBy(['f_id', 'f_file']) 158 ->orderBy(new Expression('COUNT(*)'), 'DESC') 159 ->select(['families.*']) 160 ->limit($total) 161 ->get() 162 ->map(Family::rowMapper()) 163 ->filter(GedcomRecord::accessFilter()) 164 ->map(static function (Family $family): array { 165 $count = 0; 166 foreach ($family->children() as $child) { 167 foreach ($child->spouseFamilies() as $spouse_family) { 168 $count += $spouse_family->children()->count(); 169 } 170 } 171 172 return [ 173 'family' => $family, 174 'count' => $count, 175 ]; 176 }) 177 ->all(); 178 } 179 180 /** 181 * Find the couple with the most grandchildren. 182 * 183 * @param int $total 184 * 185 * @return string 186 */ 187 public function topTenLargestGrandFamily(int $total = 10): string 188 { 189 return view('statistics/families/top10-nolist-grand', [ 190 'records' => $this->topTenGrandFamilyQuery($total), 191 ]); 192 } 193 194 /** 195 * Find the couple with the most grandchildren. 196 * 197 * @param int $total 198 * 199 * @return string 200 */ 201 public function topTenLargestGrandFamilyList(int $total = 10): string 202 { 203 return view('statistics/families/top10-list-grand', [ 204 'records' => $this->topTenGrandFamilyQuery($total), 205 ]); 206 } 207 208 /** 209 * Find the families with no children. 210 * 211 * @return int 212 */ 213 private function noChildrenFamiliesQuery(): int 214 { 215 return DB::table('families') 216 ->where('f_file', '=', $this->tree->id()) 217 ->where('f_numchil', '=', 0) 218 ->count(); 219 } 220 221 /** 222 * Find the families with no children. 223 * 224 * @return string 225 */ 226 public function noChildrenFamilies(): string 227 { 228 return I18N::number($this->noChildrenFamiliesQuery()); 229 } 230 231 /** 232 * Find the families with no children. 233 * 234 * @param string $type 235 * 236 * @return string 237 */ 238 public function noChildrenFamiliesList($type = 'list'): string 239 { 240 $families = DB::table('families') 241 ->where('f_file', '=', $this->tree->id()) 242 ->where('f_numchil', '=', 0) 243 ->get() 244 ->map(Family::rowMapper()) 245 ->filter(GedcomRecord::accessFilter()); 246 247 $top10 = []; 248 249 /** @var Family $family */ 250 foreach ($families as $family) { 251 if ($type === 'list') { 252 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>'; 253 } else { 254 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 255 } 256 } 257 258 if ($type === 'list') { 259 $top10 = implode('', $top10); 260 } else { 261 $top10 = implode('; ', $top10); 262 } 263 264 265 if ($type === 'list') { 266 return '<ul>' . $top10 . '</ul>'; 267 } 268 269 return $top10; 270 } 271 272 /** 273 * Create a chart of children with no families. 274 * 275 * @param int $year1 276 * @param int $year2 277 * 278 * @return string 279 */ 280 public function chartNoChildrenFamilies(int $year1 = -1, int $year2 = -1): string 281 { 282 $no_child_fam = $this->noChildrenFamiliesQuery(); 283 284 return (new ChartNoChildrenFamilies($this->tree)) 285 ->chartNoChildrenFamilies($no_child_fam, $year1, $year2); 286 } 287 288 /** 289 * Returns the ages between siblings. 290 * 291 * @param int $total The total number of records to query 292 * 293 * @return array 294 */ 295 private function ageBetweenSiblingsQuery(int $total): array 296 { 297 $prefix = DB::connection()->getTablePrefix(); 298 299 return DB::table('link AS link1') 300 ->join('link AS link2', static function (JoinClause $join): void { 301 $join 302 ->on('link2.l_from', '=', 'link1.l_from') 303 ->on('link2.l_type', '=', 'link1.l_type') 304 ->on('link2.l_file', '=', 'link1.l_file'); 305 }) 306 ->join('dates AS child1', static function (JoinClause $join): void { 307 $join 308 ->on('child1.d_gid', '=', 'link1.l_to') 309 ->on('child1.d_file', '=', 'link1.l_file') 310 ->where('child1.d_fact', '=', 'BIRT') 311 ->where('child1.d_julianday1', '<>', 0); 312 }) 313 ->join('dates AS child2', static function (JoinClause $join): void { 314 $join 315 ->on('child2.d_gid', '=', 'link2.l_to') 316 ->on('child2.d_file', '=', 'link2.l_file') 317 ->where('child2.d_fact', '=', 'BIRT') 318 ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1'); 319 }) 320 ->where('link1.l_type', '=', 'CHIL') 321 ->where('link1.l_file', '=', $this->tree->id()) 322 ->distinct() 323 ->select(['link1.l_from AS family', 'link1.l_to AS ch1', 'link2.l_to AS ch2', new Expression($prefix . 'child2.d_julianday2 - ' . $prefix . 'child1.d_julianday1 AS age')]) 324 ->orderBy('age', 'DESC') 325 ->take($total) 326 ->get() 327 ->all(); 328 } 329 330 /** 331 * Returns the calculated age the time of event. 332 * 333 * @param int $age The age from the database record 334 * 335 * @return string 336 */ 337 private function calculateAge(int $age): string 338 { 339 if ((int) ($age / 365.25) > 0) { 340 $result = (int) ($age / 365.25) . 'y'; 341 } elseif ((int) ($age / 30.4375) > 0) { 342 $result = (int) ($age / 30.4375) . 'm'; 343 } else { 344 $result = $age . 'd'; 345 } 346 347 return FunctionsDate::getAgeAtEvent($result); 348 } 349 350 /** 351 * Find the ages between siblings. 352 * 353 * @param int $total The total number of records to query 354 * 355 * @return array 356 * @throws Exception 357 */ 358 private function ageBetweenSiblingsNoList(int $total): array 359 { 360 $rows = $this->ageBetweenSiblingsQuery($total); 361 362 foreach ($rows as $fam) { 363 $family = Family::getInstance($fam->family, $this->tree); 364 $child1 = Individual::getInstance($fam->ch1, $this->tree); 365 $child2 = Individual::getInstance($fam->ch2, $this->tree); 366 367 if ($child1->canShow() && $child2->canShow()) { 368 // ! Single array (no list) 369 return [ 370 'child1' => $child1, 371 'child2' => $child2, 372 'family' => $family, 373 'age' => $this->calculateAge((int) $fam->age), 374 ]; 375 } 376 } 377 378 return []; 379 } 380 381 /** 382 * Find the ages between siblings. 383 * 384 * @param int $total The total number of records to query 385 * @param bool $one Include each family only once if true 386 * 387 * @return array 388 * @throws Exception 389 */ 390 private function ageBetweenSiblingsList(int $total, bool $one): array 391 { 392 $rows = $this->ageBetweenSiblingsQuery($total); 393 $top10 = []; 394 $dist = []; 395 396 foreach ($rows as $fam) { 397 $family = Family::getInstance($fam->family, $this->tree); 398 $child1 = Individual::getInstance($fam->ch1, $this->tree); 399 $child2 = Individual::getInstance($fam->ch2, $this->tree); 400 401 $age = $this->calculateAge((int) $fam->age); 402 403 if ($one && !in_array($fam->family, $dist, true)) { 404 if ($child1->canShow() && $child2->canShow()) { 405 $top10[] = [ 406 'child1' => $child1, 407 'child2' => $child2, 408 'family' => $family, 409 'age' => $age, 410 ]; 411 412 $dist[] = $fam->family; 413 } 414 } elseif (!$one && $child1->canShow() && $child2->canShow()) { 415 $top10[] = [ 416 'child1' => $child1, 417 'child2' => $child2, 418 'family' => $family, 419 'age' => $age, 420 ]; 421 } 422 } 423 424 return $top10; 425 } 426 427 /** 428 * Find the ages between siblings. 429 * 430 * @param int $total The total number of records to query 431 * 432 * @return string 433 */ 434 private function ageBetweenSiblingsAge(int $total): string 435 { 436 $rows = $this->ageBetweenSiblingsQuery($total); 437 438 foreach ($rows as $fam) { 439 return $this->calculateAge((int) $fam->age); 440 } 441 442 return ''; 443 } 444 445 /** 446 * Find the ages between siblings. 447 * 448 * @param int $total The total number of records to query 449 * 450 * @return string 451 * @throws Exception 452 */ 453 private function ageBetweenSiblingsName(int $total): string 454 { 455 $rows = $this->ageBetweenSiblingsQuery($total); 456 457 foreach ($rows as $fam) { 458 $family = Family::getInstance($fam->family, $this->tree); 459 $child1 = Individual::getInstance($fam->ch1, $this->tree); 460 $child2 = Individual::getInstance($fam->ch2, $this->tree); 461 462 if ($child1->canShow() && $child2->canShow()) { 463 $return = '<a href="' . e($child2->url()) . '">' . $child2->fullName() . '</a> '; 464 $return .= I18N::translate('and') . ' '; 465 $return .= '<a href="' . e($child1->url()) . '">' . $child1->fullName() . '</a>'; 466 $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; 467 } else { 468 $return = I18N::translate('This information is private and cannot be shown.'); 469 } 470 471 return $return; 472 } 473 474 return ''; 475 } 476 477 /** 478 * Find the names of siblings with the widest age gap. 479 * 480 * @param int $total 481 * 482 * @return string 483 */ 484 public function topAgeBetweenSiblingsName(int $total = 10): string 485 { 486 return $this->ageBetweenSiblingsName($total); 487 } 488 489 /** 490 * Find the widest age gap between siblings. 491 * 492 * @param int $total 493 * 494 * @return string 495 */ 496 public function topAgeBetweenSiblings(int $total = 10): string 497 { 498 return $this->ageBetweenSiblingsAge($total); 499 } 500 501 /** 502 * Find the name of siblings with the widest age gap. 503 * 504 * @param int $total 505 * 506 * @return string 507 */ 508 public function topAgeBetweenSiblingsFullName(int $total = 10): string 509 { 510 $record = $this->ageBetweenSiblingsNoList($total); 511 512 if (empty($record)) { 513 return I18N::translate('This information is not available.'); 514 } 515 516 return view('statistics/families/top10-nolist-age', [ 517 'record' => $record, 518 ]); 519 } 520 521 /** 522 * Find the siblings with the widest age gaps. 523 * 524 * @param int $total 525 * @param string $one 526 * 527 * @return string 528 */ 529 public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string 530 { 531 $records = $this->ageBetweenSiblingsList($total, (bool) $one); 532 533 return view('statistics/families/top10-list-age', [ 534 'records' => $records, 535 ]); 536 } 537 538 /** 539 * General query on familes/children. 540 * 541 * @param int $year1 542 * @param int $year2 543 * 544 * @return stdClass[] 545 */ 546 public function statsChildrenQuery(int $year1 = -1, int $year2 = -1): array 547 { 548 $query = DB::table('families') 549 ->where('f_file', '=', $this->tree->id()) 550 ->groupBy(['f_numchil']) 551 ->select(['f_numchil', new Expression('COUNT(*) AS total')]); 552 553 if ($year1 >= 0 && $year2 >= 0) { 554 $query 555 ->join('dates', static function (JoinClause $join): void { 556 $join 557 ->on('d_file', '=', 'f_file') 558 ->on('d_gid', '=', 'f_id'); 559 }) 560 ->where('d_fact', '=', 'MARR') 561 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 562 ->whereBetween('d_year', [$year1, $year2]); 563 } 564 565 return $query->get()->all(); 566 } 567 568 /** 569 * Genearl query on families/children. 570 * 571 * @return string 572 */ 573 public function statsChildren(): string 574 { 575 return (new ChartChildren($this->tree)) 576 ->chartChildren(); 577 } 578 579 /** 580 * Count the total children. 581 * 582 * @return string 583 */ 584 public function totalChildren(): string 585 { 586 $total = (int) DB::table('families') 587 ->where('f_file', '=', $this->tree->id()) 588 ->sum('f_numchil'); 589 590 return I18N::number($total); 591 } 592 593 /** 594 * Find the average number of children in families. 595 * 596 * @return string 597 */ 598 public function averageChildren(): string 599 { 600 $average = (float) DB::table('families') 601 ->where('f_file', '=', $this->tree->id()) 602 ->avg('f_numchil'); 603 604 return I18N::number($average, 2); 605 } 606 607 /** 608 * General query on families. 609 * 610 * @param int $total 611 * 612 * @return array 613 */ 614 private function topTenFamilyQuery(int $total): array 615 { 616 return DB::table('families') 617 ->where('f_file', '=', $this->tree->id()) 618 ->orderBy('f_numchil', 'DESC') 619 ->limit($total) 620 ->get() 621 ->map(Family::rowMapper()) 622 ->filter(GedcomRecord::accessFilter()) 623 ->map(static function (Family $family): array { 624 return [ 625 'family' => $family, 626 'count' => $family->numberOfChildren(), 627 ]; 628 }) 629 ->all(); 630 } 631 632 /** 633 * The the families with the most children. 634 * 635 * @param int $total 636 * 637 * @return string 638 */ 639 public function topTenLargestFamily(int $total = 10): string 640 { 641 $records = $this->topTenFamilyQuery($total); 642 643 return view('statistics/families/top10-nolist', [ 644 'records' => $records, 645 ]); 646 } 647 648 /** 649 * Find the families with the most children. 650 * 651 * @param int $total 652 * 653 * @return string 654 */ 655 public function topTenLargestFamilyList(int $total = 10): string 656 { 657 $records = $this->topTenFamilyQuery($total); 658 659 return view('statistics/families/top10-list', [ 660 'records' => $records, 661 ]); 662 } 663 664 /** 665 * Create a chart of the largest families. 666 * 667 * @param string|null $color_from 668 * @param string|null $color_to 669 * @param int $total 670 * 671 * @return string 672 */ 673 public function chartLargestFamilies( 674 string $color_from = null, 675 string $color_to = null, 676 int $total = 10 677 ): string { 678 return (new ChartFamilyLargest($this->tree)) 679 ->chartLargestFamilies($color_from, $color_to, $total); 680 } 681 682 /** 683 * Find the month in the year of the birth of the first child. 684 * 685 * @param int $year1 686 * @param int $year2 687 * 688 * @return Builder 689 */ 690 public function monthFirstChildQuery(int $year1 = -1, int $year2 = -1): Builder 691 { 692 $first_child_subquery = DB::table('link') 693 ->join('dates', static function (JoinClause $join): void { 694 $join 695 ->on('d_gid', '=', 'l_to') 696 ->on('d_file', '=', 'l_file') 697 ->where('d_julianday1', '<>', 0) 698 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']); 699 }) 700 ->where('l_file', '=', $this->tree->id()) 701 ->where('l_type', '=', 'CHIL') 702 ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')]) 703 ->groupBy(['family_id']); 704 705 $query = DB::table('link') 706 ->join('dates', static function (JoinClause $join): void { 707 $join 708 ->on('d_gid', '=', 'l_to') 709 ->on('d_file', '=', 'l_file'); 710 }) 711 ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void { 712 $join 713 ->on('family_id', '=', 'l_from') 714 ->on('min_birth_jd', '=', 'd_julianday1'); 715 }) 716 ->where('link.l_file', '=', $this->tree->id()) 717 ->where('link.l_type', '=', 'CHIL') 718 ->select(['d_month', new Expression('COUNT(*) AS total')]) 719 ->groupBy(['d_month']); 720 721 if ($year1 >= 0 && $year2 >= 0) { 722 $query->whereBetween('d_year', [$year1, $year2]); 723 } 724 725 return $query; 726 } 727 728 /** 729 * Find the month in the year of the birth of the first child. 730 * 731 * @param int $year1 732 * @param int $year2 733 * 734 * @return Builder 735 */ 736 public function monthFirstChildBySexQuery(int $year1 = -1, int $year2 = -1): Builder 737 { 738 return $this->monthFirstChildQuery($year1, $year2) 739 ->join('individuals', static function (JoinClause $join): void { 740 $join 741 ->on('i_file', '=', 'l_file') 742 ->on('i_id', '=', 'l_to'); 743 }) 744 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 745 ->groupBy(['d_month', 'i_sex']); 746 } 747 748 /** 749 * Number of husbands. 750 * 751 * @return string 752 */ 753 public function totalMarriedMales(): string 754 { 755 $n = (int) DB::table('families') 756 ->where('f_file', '=', $this->tree->id()) 757 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 758 ->distinct() 759 ->count('f_husb'); 760 761 return I18N::number($n); 762 } 763 764 /** 765 * Number of wives. 766 * 767 * @return string 768 */ 769 public function totalMarriedFemales(): string 770 { 771 $n = (int) DB::table('families') 772 ->where('f_file', '=', $this->tree->id()) 773 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 774 ->distinct() 775 ->count('f_wife'); 776 777 return I18N::number($n); 778 } 779 780 /** 781 * General query on parents. 782 * 783 * @param string $type 784 * @param string $age_dir 785 * @param string $sex 786 * @param bool $show_years 787 * 788 * @return string 789 */ 790 private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string 791 { 792 if ($sex === 'F') { 793 $sex_field = 'WIFE'; 794 } else { 795 $sex_field = 'HUSB'; 796 } 797 798 if ($age_dir !== 'ASC') { 799 $age_dir = 'DESC'; 800 } 801 802 $prefix = DB::connection()->getTablePrefix(); 803 804 $row = DB::table('link AS parentfamily') 805 ->join('link AS childfamily', static function (JoinClause $join): void { 806 $join 807 ->on('childfamily.l_file', '=', 'parentfamily.l_file') 808 ->on('childfamily.l_from', '=', 'parentfamily.l_from') 809 ->where('childfamily.l_type', '=', 'CHIL'); 810 }) 811 ->join('dates AS birth', static function (JoinClause $join): void { 812 $join 813 ->on('birth.d_file', '=', 'parentfamily.l_file') 814 ->on('birth.d_gid', '=', 'parentfamily.l_to') 815 ->where('birth.d_fact', '=', 'BIRT') 816 ->where('birth.d_julianday1', '<>', 0); 817 }) 818 ->join('dates AS childbirth', static function (JoinClause $join): void { 819 $join 820 ->on('childbirth.d_file', '=', 'parentfamily.l_file') 821 ->on('childbirth.d_gid', '=', 'childfamily.l_to') 822 ->where('childbirth.d_fact', '=', 'BIRT'); 823 }) 824 ->where('childfamily.l_file', '=', $this->tree->id()) 825 ->where('parentfamily.l_type', '=', $sex_field) 826 ->where('childbirth.d_julianday2', '>', 'birth.d_julianday1') 827 ->select(['parentfamily.l_to AS id', new Expression($prefix . 'childbirth.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]) 828 ->take(1) 829 ->orderBy('age', $age_dir) 830 ->get() 831 ->first(); 832 833 if ($row === null) { 834 return ''; 835 } 836 837 $person = Individual::getInstance($row->id, $this->tree); 838 839 switch ($type) { 840 default: 841 case 'full': 842 if ($person && $person->canShow()) { 843 $result = $person->formatList(); 844 } else { 845 $result = I18N::translate('This information is private and cannot be shown.'); 846 } 847 break; 848 849 case 'name': 850 $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>'; 851 break; 852 853 case 'age': 854 $age = $row->age; 855 856 if ($show_years) { 857 $result = $this->calculateAge((int) $row->age); 858 } else { 859 $result = (string) floor($age / 365.25); 860 } 861 862 break; 863 } 864 865 return $result; 866 } 867 868 /** 869 * Find the youngest mother 870 * 871 * @return string 872 */ 873 public function youngestMother(): string 874 { 875 return $this->parentsQuery('full', 'ASC', 'F', false); 876 } 877 878 /** 879 * Find the name of the youngest mother. 880 * 881 * @return string 882 */ 883 public function youngestMotherName(): string 884 { 885 return $this->parentsQuery('name', 'ASC', 'F', false); 886 } 887 888 /** 889 * Find the age of the youngest mother. 890 * 891 * @param string $show_years 892 * 893 * @return string 894 */ 895 public function youngestMotherAge(string $show_years = ''): string 896 { 897 return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); 898 } 899 900 /** 901 * Find the oldest mother. 902 * 903 * @return string 904 */ 905 public function oldestMother(): string 906 { 907 return $this->parentsQuery('full', 'DESC', 'F', false); 908 } 909 910 /** 911 * Find the name of the oldest mother. 912 * 913 * @return string 914 */ 915 public function oldestMotherName(): string 916 { 917 return $this->parentsQuery('name', 'DESC', 'F', false); 918 } 919 920 /** 921 * Find the age of the oldest mother. 922 * 923 * @param string $show_years 924 * 925 * @return string 926 */ 927 public function oldestMotherAge(string $show_years = ''): string 928 { 929 return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); 930 } 931 932 /** 933 * Find the youngest father. 934 * 935 * @return string 936 */ 937 public function youngestFather(): string 938 { 939 return $this->parentsQuery('full', 'ASC', 'M', false); 940 } 941 942 /** 943 * Find the name of the youngest father. 944 * 945 * @return string 946 */ 947 public function youngestFatherName(): string 948 { 949 return $this->parentsQuery('name', 'ASC', 'M', false); 950 } 951 952 /** 953 * Find the age of the youngest father. 954 * 955 * @param string $show_years 956 * 957 * @return string 958 */ 959 public function youngestFatherAge(string $show_years = ''): string 960 { 961 return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); 962 } 963 964 /** 965 * Find the oldest father. 966 * 967 * @return string 968 */ 969 public function oldestFather(): string 970 { 971 return $this->parentsQuery('full', 'DESC', 'M', false); 972 } 973 974 /** 975 * Find the name of the oldest father. 976 * 977 * @return string 978 */ 979 public function oldestFatherName(): string 980 { 981 return $this->parentsQuery('name', 'DESC', 'M', false); 982 } 983 984 /** 985 * Find the age of the oldest father. 986 * 987 * @param string $show_years 988 * 989 * @return string 990 */ 991 public function oldestFatherAge(string $show_years = ''): string 992 { 993 return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); 994 } 995 996 /** 997 * General query on age at marriage. 998 * 999 * @param string $type 1000 * @param string $age_dir "ASC" or "DESC" 1001 * @param int $total 1002 * 1003 * @return string 1004 */ 1005 private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string 1006 { 1007 $prefix = DB::connection()->getTablePrefix(); 1008 1009 $hrows = DB::table('families') 1010 ->where('f_file', '=', $this->tree->id()) 1011 ->join('dates AS married', static function (JoinClause $join): void { 1012 $join 1013 ->on('married.d_file', '=', 'f_file') 1014 ->on('married.d_gid', '=', 'f_id') 1015 ->where('married.d_fact', '=', 'MARR') 1016 ->where('married.d_julianday1', '<>', 0); 1017 }) 1018 ->join('dates AS husbdeath', static function (JoinClause $join): void { 1019 $join 1020 ->on('husbdeath.d_gid', '=', 'f_husb') 1021 ->on('husbdeath.d_file', '=', 'f_file') 1022 ->where('husbdeath.d_fact', '=', 'DEAT'); 1023 }) 1024 ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2') 1025 ->groupBy(['f_id']) 1026 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'husbdeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1027 ->get() 1028 ->all(); 1029 1030 $wrows = DB::table('families') 1031 ->where('f_file', '=', $this->tree->id()) 1032 ->join('dates AS married', static function (JoinClause $join): void { 1033 $join 1034 ->on('married.d_file', '=', 'f_file') 1035 ->on('married.d_gid', '=', 'f_id') 1036 ->where('married.d_fact', '=', 'MARR') 1037 ->where('married.d_julianday1', '<>', 0); 1038 }) 1039 ->join('dates AS wifedeath', static function (JoinClause $join): void { 1040 $join 1041 ->on('wifedeath.d_gid', '=', 'f_wife') 1042 ->on('wifedeath.d_file', '=', 'f_file') 1043 ->where('wifedeath.d_fact', '=', 'DEAT'); 1044 }) 1045 ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2') 1046 ->groupBy(['f_id']) 1047 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'wifedeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1048 ->get() 1049 ->all(); 1050 1051 $drows = DB::table('families') 1052 ->where('f_file', '=', $this->tree->id()) 1053 ->join('dates AS married', static function (JoinClause $join): void { 1054 $join 1055 ->on('married.d_file', '=', 'f_file') 1056 ->on('married.d_gid', '=', 'f_id') 1057 ->where('married.d_fact', '=', 'MARR') 1058 ->where('married.d_julianday1', '<>', 0); 1059 }) 1060 ->join('dates AS divorced', static function (JoinClause $join): void { 1061 $join 1062 ->on('divorced.d_gid', '=', 'f_id') 1063 ->on('divorced.d_file', '=', 'f_file') 1064 ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']); 1065 }) 1066 ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2') 1067 ->groupBy(['f_id']) 1068 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'divorced.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1069 ->get() 1070 ->all(); 1071 1072 $rows = []; 1073 foreach ($drows as $family) { 1074 $rows[$family->family] = $family->age; 1075 } 1076 1077 foreach ($hrows as $family) { 1078 if (!isset($rows[$family->family])) { 1079 $rows[$family->family] = $family->age; 1080 } 1081 } 1082 1083 foreach ($wrows as $family) { 1084 if (!isset($rows[$family->family])) { 1085 $rows[$family->family] = $family->age; 1086 } elseif ($rows[$family->family] > $family->age) { 1087 $rows[$family->family] = $family->age; 1088 } 1089 } 1090 1091 if ($age_dir === 'DESC') { 1092 arsort($rows); 1093 } else { 1094 asort($rows); 1095 } 1096 1097 $top10 = []; 1098 $i = 0; 1099 foreach ($rows as $xref => $age) { 1100 $family = Family::getInstance((string) $xref, $this->tree); 1101 if ($type === 'name') { 1102 return $family->formatList(); 1103 } 1104 1105 $age = $this->calculateAge((int) $age); 1106 1107 if ($type === 'age') { 1108 return $age; 1109 } 1110 1111 $husb = $family->husband(); 1112 $wife = $family->wife(); 1113 1114 if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) && ($wife && ($wife->getAllDeathDates() || !$wife->isDead()))) { 1115 if ($family && $family->canShow()) { 1116 if ($type === 'list') { 1117 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>'; 1118 } else { 1119 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')'; 1120 } 1121 } 1122 if (++$i === $total) { 1123 break; 1124 } 1125 } 1126 } 1127 1128 if ($type === 'list') { 1129 $top10 = implode('', $top10); 1130 } else { 1131 $top10 = implode('; ', $top10); 1132 } 1133 1134 if (I18N::direction() === 'rtl') { 1135 $top10 = str_replace([ 1136 '[', 1137 ']', 1138 '(', 1139 ')', 1140 '+', 1141 ], [ 1142 '‏[', 1143 '‏]', 1144 '‏(', 1145 '‏)', 1146 '‏+', 1147 ], $top10); 1148 } 1149 1150 if ($type === 'list') { 1151 return '<ul>' . $top10 . '</ul>'; 1152 } 1153 1154 return $top10; 1155 } 1156 1157 /** 1158 * General query on marriage ages. 1159 * 1160 * @return string 1161 */ 1162 public function topAgeOfMarriageFamily(): string 1163 { 1164 return $this->ageOfMarriageQuery('name', 'DESC', 1); 1165 } 1166 1167 /** 1168 * General query on marriage ages. 1169 * 1170 * @return string 1171 */ 1172 public function topAgeOfMarriage(): string 1173 { 1174 return $this->ageOfMarriageQuery('age', 'DESC', 1); 1175 } 1176 1177 /** 1178 * General query on marriage ages. 1179 * 1180 * @param int $total 1181 * 1182 * @return string 1183 */ 1184 public function topAgeOfMarriageFamilies(int $total = 10): string 1185 { 1186 return $this->ageOfMarriageQuery('nolist', 'DESC', $total); 1187 } 1188 1189 /** 1190 * General query on marriage ages. 1191 * 1192 * @param int $total 1193 * 1194 * @return string 1195 */ 1196 public function topAgeOfMarriageFamiliesList(int $total = 10): string 1197 { 1198 return $this->ageOfMarriageQuery('list', 'DESC', $total); 1199 } 1200 1201 /** 1202 * General query on marriage ages. 1203 * 1204 * @return string 1205 */ 1206 public function minAgeOfMarriageFamily(): string 1207 { 1208 return $this->ageOfMarriageQuery('name', 'ASC', 1); 1209 } 1210 1211 /** 1212 * General query on marriage ages. 1213 * 1214 * @return string 1215 */ 1216 public function minAgeOfMarriage(): string 1217 { 1218 return $this->ageOfMarriageQuery('age', 'ASC', 1); 1219 } 1220 1221 /** 1222 * General query on marriage ages. 1223 * 1224 * @param int $total 1225 * 1226 * @return string 1227 */ 1228 public function minAgeOfMarriageFamilies(int $total = 10): string 1229 { 1230 return $this->ageOfMarriageQuery('nolist', 'ASC', $total); 1231 } 1232 1233 /** 1234 * General query on marriage ages. 1235 * 1236 * @param int $total 1237 * 1238 * @return string 1239 */ 1240 public function minAgeOfMarriageFamiliesList(int $total = 10): string 1241 { 1242 return $this->ageOfMarriageQuery('list', 'ASC', $total); 1243 } 1244 1245 /** 1246 * Find the ages between spouses. 1247 * 1248 * @param string $age_dir 1249 * @param int $total 1250 * 1251 * @return array 1252 */ 1253 private function ageBetweenSpousesQuery(string $age_dir, int $total): array 1254 { 1255 $prefix = DB::connection()->getTablePrefix(); 1256 1257 $query = DB::table('families') 1258 ->where('f_file', '=', $this->tree->id()) 1259 ->join('dates AS wife', static function (JoinClause $join): void { 1260 $join 1261 ->on('wife.d_gid', '=', 'f_wife') 1262 ->on('wife.d_file', '=', 'f_file') 1263 ->where('wife.d_fact', '=', 'BIRT') 1264 ->where('wife.d_julianday1', '<>', 0); 1265 }) 1266 ->join('dates AS husb', static function (JoinClause $join): void { 1267 $join 1268 ->on('husb.d_gid', '=', 'f_husb') 1269 ->on('husb.d_file', '=', 'f_file') 1270 ->where('husb.d_fact', '=', 'BIRT') 1271 ->where('husb.d_julianday1', '<>', 0); 1272 }); 1273 1274 if ($age_dir === 'DESC') { 1275 $query 1276 ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1') 1277 ->orderBy(new Expression('MIN(' . $prefix . 'wife.d_julianday1) - MIN(' . $prefix . 'husb.d_julianday1)'), 'DESC'); 1278 } else { 1279 $query 1280 ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1') 1281 ->orderBy(new Expression('MIN(' . $prefix . 'husb.d_julianday1) - MIN(' . $prefix . 'wife.d_julianday1)'), 'DESC'); 1282 } 1283 1284 return $query 1285 ->groupBy(['f_id', 'f_file']) 1286 ->select(['families.*']) 1287 ->take($total) 1288 ->get() 1289 ->map(Family::rowMapper()) 1290 ->filter(GedcomRecord::accessFilter()) 1291 ->map(function (Family $family) use ($age_dir): array { 1292 $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay(); 1293 $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay(); 1294 1295 if ($age_dir === 'DESC') { 1296 $diff = $wife_birt_jd - $husb_birt_jd; 1297 } else { 1298 $diff = $husb_birt_jd - $wife_birt_jd; 1299 } 1300 1301 return [ 1302 'family' => $family, 1303 'age' => $this->calculateAge($diff), 1304 ]; 1305 }) 1306 ->all(); 1307 } 1308 1309 /** 1310 * Find the age between husband and wife. 1311 * 1312 * @param int $total 1313 * 1314 * @return string 1315 */ 1316 public function ageBetweenSpousesMF(int $total = 10): string 1317 { 1318 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1319 1320 return view('statistics/families/top10-nolist-spouses', [ 1321 'records' => $records, 1322 ]); 1323 } 1324 1325 /** 1326 * Find the age between husband and wife. 1327 * 1328 * @param int $total 1329 * 1330 * @return string 1331 */ 1332 public function ageBetweenSpousesMFList(int $total = 10): string 1333 { 1334 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1335 1336 return view('statistics/families/top10-list-spouses', [ 1337 'records' => $records, 1338 ]); 1339 } 1340 1341 /** 1342 * Find the age between wife and husband.. 1343 * 1344 * @param int $total 1345 * 1346 * @return string 1347 */ 1348 public function ageBetweenSpousesFM(int $total = 10): string 1349 { 1350 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1351 1352 return view('statistics/families/top10-nolist-spouses', [ 1353 'records' => $records, 1354 ]); 1355 } 1356 1357 /** 1358 * Find the age between wife and husband.. 1359 * 1360 * @param int $total 1361 * 1362 * @return string 1363 */ 1364 public function ageBetweenSpousesFMList(int $total = 10): string 1365 { 1366 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1367 1368 return view('statistics/families/top10-list-spouses', [ 1369 'records' => $records, 1370 ]); 1371 } 1372 1373 /** 1374 * General query on ages at marriage. 1375 * 1376 * @param string $sex "M" or "F" 1377 * @param int $year1 1378 * @param int $year2 1379 * 1380 * @return array 1381 */ 1382 public function statsMarrAgeQuery($sex, $year1 = -1, $year2 = -1): array 1383 { 1384 $prefix = DB::connection()->getTablePrefix(); 1385 1386 $query = DB::table('dates AS married') 1387 ->join('families', static function (JoinClause $join): void { 1388 $join 1389 ->on('f_file', '=', 'married.d_file') 1390 ->on('f_id', '=', 'married.d_gid'); 1391 }) 1392 ->join('dates AS birth', static function (JoinClause $join) use ($sex): void { 1393 $join 1394 ->on('birth.d_file', '=', 'married.d_file') 1395 ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife') 1396 ->where('birth.d_julianday1', '<>', 0) 1397 ->where('birth.d_fact', '=', 'BIRT') 1398 ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']); 1399 }) 1400 ->where('married.d_file', '=', $this->tree->id()) 1401 ->where('married.d_fact', '=', 'MARR') 1402 ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 1403 ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1') 1404 ->select(['f_id', 'birth.d_gid', new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]); 1405 1406 if ($year1 >= 0 && $year2 >= 0) { 1407 $query->whereBetween('married.d_year', [$year1, $year2]); 1408 } 1409 1410 return $query 1411 ->get() 1412 ->map(static function (stdClass $row): stdClass { 1413 $row->age = (int) $row->age; 1414 1415 return $row; 1416 }) 1417 ->all(); 1418 } 1419 1420 /** 1421 * General query on marriage ages. 1422 * 1423 * @return string 1424 */ 1425 public function statsMarrAge(): string 1426 { 1427 return (new ChartMarriageAge($this->tree)) 1428 ->chartMarriageAge(); 1429 } 1430 1431 /** 1432 * Query the database for marriage tags. 1433 * 1434 * @param string $type "full", "name" or "age" 1435 * @param string $age_dir "ASC" or "DESC" 1436 * @param string $sex "F" or "M" 1437 * @param bool $show_years 1438 * 1439 * @return string 1440 */ 1441 private function marriageQuery(string $type, string $age_dir, string $sex, bool $show_years): string 1442 { 1443 if ($sex === 'F') { 1444 $sex_field = 'f_wife'; 1445 } else { 1446 $sex_field = 'f_husb'; 1447 } 1448 1449 if ($age_dir !== 'ASC') { 1450 $age_dir = 'DESC'; 1451 } 1452 1453 $prefix = DB::connection()->getTablePrefix(); 1454 1455 $row = DB::table('families') 1456 ->join('dates AS married', static function (JoinClause $join): void { 1457 $join 1458 ->on('married.d_file', '=', 'f_file') 1459 ->on('married.d_gid', '=', 'f_id') 1460 ->where('married.d_fact', '=', 'MARR'); 1461 }) 1462 ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void { 1463 $join 1464 ->on('i_file', '=', 'f_file') 1465 ->on('i_id', '=', $sex_field) 1466 ->where('i_sex', '=', $sex); 1467 }) 1468 ->join('dates AS birth', static function (JoinClause $join): void { 1469 $join 1470 ->on('birth.d_file', '=', 'i_file') 1471 ->on('birth.d_gid', '=', 'i_id') 1472 ->where('birth.d_fact', '=', 'BIRT') 1473 ->where('birth.d_julianday1', '<>', 0); 1474 }) 1475 ->where('f_file', '=', $this->tree->id()) 1476 ->where('married.d_julianday2', '>', 'birth.d_julianday1') 1477 ->orderBy(new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1'), $age_dir) 1478 ->select(['f_id AS famid', $sex_field, new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age'), 'i_id']) 1479 ->take(1) 1480 ->get() 1481 ->first(); 1482 1483 if ($row === null) { 1484 return ''; 1485 } 1486 1487 $family = Family::getInstance($row->famid, $this->tree); 1488 $person = Individual::getInstance($row->i_id, $this->tree); 1489 1490 switch ($type) { 1491 default: 1492 case 'full': 1493 if ($family && $family->canShow()) { 1494 $result = $family->formatList(); 1495 } else { 1496 $result = I18N::translate('This information is private and cannot be shown.'); 1497 } 1498 break; 1499 1500 case 'name': 1501 $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>'; 1502 break; 1503 1504 case 'age': 1505 $age = $row->age; 1506 1507 if ($show_years) { 1508 $result = $this->calculateAge((int) $row->age); 1509 } else { 1510 $result = I18N::number((int) ($age / 365.25)); 1511 } 1512 1513 break; 1514 } 1515 1516 return $result; 1517 } 1518 1519 /** 1520 * Find the youngest wife. 1521 * 1522 * @return string 1523 */ 1524 public function youngestMarriageFemale(): string 1525 { 1526 return $this->marriageQuery('full', 'ASC', 'F', false); 1527 } 1528 1529 /** 1530 * Find the name of the youngest wife. 1531 * 1532 * @return string 1533 */ 1534 public function youngestMarriageFemaleName(): string 1535 { 1536 return $this->marriageQuery('name', 'ASC', 'F', false); 1537 } 1538 1539 /** 1540 * Find the age of the youngest wife. 1541 * 1542 * @param string $show_years 1543 * 1544 * @return string 1545 */ 1546 public function youngestMarriageFemaleAge(string $show_years = ''): string 1547 { 1548 return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); 1549 } 1550 1551 /** 1552 * Find the oldest wife. 1553 * 1554 * @return string 1555 */ 1556 public function oldestMarriageFemale(): string 1557 { 1558 return $this->marriageQuery('full', 'DESC', 'F', false); 1559 } 1560 1561 /** 1562 * Find the name of the oldest wife. 1563 * 1564 * @return string 1565 */ 1566 public function oldestMarriageFemaleName(): string 1567 { 1568 return $this->marriageQuery('name', 'DESC', 'F', false); 1569 } 1570 1571 /** 1572 * Find the age of the oldest wife. 1573 * 1574 * @param string $show_years 1575 * 1576 * @return string 1577 */ 1578 public function oldestMarriageFemaleAge(string $show_years = ''): string 1579 { 1580 return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); 1581 } 1582 1583 /** 1584 * Find the youngest husband. 1585 * 1586 * @return string 1587 */ 1588 public function youngestMarriageMale(): string 1589 { 1590 return $this->marriageQuery('full', 'ASC', 'M', false); 1591 } 1592 1593 /** 1594 * Find the name of the youngest husband. 1595 * 1596 * @return string 1597 */ 1598 public function youngestMarriageMaleName(): string 1599 { 1600 return $this->marriageQuery('name', 'ASC', 'M', false); 1601 } 1602 1603 /** 1604 * Find the age of the youngest husband. 1605 * 1606 * @param string $show_years 1607 * 1608 * @return string 1609 */ 1610 public function youngestMarriageMaleAge(string $show_years = ''): string 1611 { 1612 return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); 1613 } 1614 1615 /** 1616 * Find the oldest husband. 1617 * 1618 * @return string 1619 */ 1620 public function oldestMarriageMale(): string 1621 { 1622 return $this->marriageQuery('full', 'DESC', 'M', false); 1623 } 1624 1625 /** 1626 * Find the name of the oldest husband. 1627 * 1628 * @return string 1629 */ 1630 public function oldestMarriageMaleName(): string 1631 { 1632 return $this->marriageQuery('name', 'DESC', 'M', false); 1633 } 1634 1635 /** 1636 * Find the age of the oldest husband. 1637 * 1638 * @param string $show_years 1639 * 1640 * @return string 1641 */ 1642 public function oldestMarriageMaleAge(string $show_years = ''): string 1643 { 1644 return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); 1645 } 1646 1647 /** 1648 * General query on marriages. 1649 * 1650 * @param int $year1 1651 * @param int $year2 1652 * 1653 * @return Builder 1654 */ 1655 public function statsMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1656 { 1657 $query = DB::table('dates') 1658 ->where('d_file', '=', $this->tree->id()) 1659 ->where('d_fact', '=', 'MARR') 1660 ->select(['d_month', new Expression('COUNT(*) AS total')]) 1661 ->groupBy(['d_month']); 1662 1663 if ($year1 >= 0 && $year2 >= 0) { 1664 $query->whereBetween('d_year', [$year1, $year2]); 1665 } 1666 1667 return $query; 1668 } 1669 1670 /** 1671 * General query on marriages. 1672 * 1673 * @param int $year1 1674 * @param int $year2 1675 * 1676 * @return Builder 1677 */ 1678 public function statsFirstMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1679 { 1680 $query = DB::table('families') 1681 ->join('dates', static function (JoinClause $join): void { 1682 $join 1683 ->on('d_gid', '=', 'f_id') 1684 ->on('d_file', '=', 'f_file') 1685 ->where('d_fact', '=', 'MARR') 1686 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']) 1687 ->where('d_julianday2', '<>', 0); 1688 })->join('individuals', static function (JoinClause $join): void { 1689 $join 1690 ->on('i_file', '=', 'f_file'); 1691 }) 1692 ->where('f_file', '=', $this->tree->id()) 1693 ->where(static function (Builder $query): void { 1694 $query 1695 ->whereColumn('i_id', '=', 'f_husb') 1696 ->orWhereColumn('i_id', '=', 'f_wife'); 1697 }); 1698 1699 if ($year1 >= 0 && $year2 >= 0) { 1700 $query->whereBetween('d_year', [$year1, $year2]); 1701 } 1702 1703 return $query 1704 ->select(['f_id AS fams', 'f_husb', 'f_wife', 'd_julianday2 AS age', 'd_month AS month', 'i_id AS indi']) 1705 ->orderBy('f_id') 1706 ->orderBy('i_id') 1707 ->orderBy('d_julianday2'); 1708 } 1709 1710 /** 1711 * General query on marriages. 1712 * 1713 * @param string|null $color_from 1714 * @param string|null $color_to 1715 * 1716 * @return string 1717 */ 1718 public function statsMarr(string $color_from = null, string $color_to = null): string 1719 { 1720 return (new ChartMarriage($this->tree)) 1721 ->chartMarriage($color_from, $color_to); 1722 } 1723 1724 /** 1725 * General divorce query. 1726 * 1727 * @param string|null $color_from 1728 * @param string|null $color_to 1729 * 1730 * @return string 1731 */ 1732 public function statsDiv(string $color_from = null, string $color_to = null): string 1733 { 1734 return (new ChartDivorce($this->tree)) 1735 ->chartDivorce($color_from, $color_to); 1736 } 1737} 1738