1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Statistics\Repository; 19 20use Exception; 21use Fisharebest\Webtrees\Family; 22use Fisharebest\Webtrees\Functions\FunctionsDate; 23use Fisharebest\Webtrees\GedcomRecord; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Individual; 26use Fisharebest\Webtrees\Statistics\Google\ChartChildren; 27use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; 28use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; 29use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; 30use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; 31use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; 32use Fisharebest\Webtrees\Tree; 33use Illuminate\Database\Capsule\Manager as DB; 34use Illuminate\Database\Query\Builder; 35use Illuminate\Database\Query\Expression; 36use Illuminate\Database\Query\JoinClause; 37use function in_array; 38use stdClass; 39 40/** 41 * 42 */ 43class FamilyRepository 44{ 45 /** 46 * @var Tree 47 */ 48 private $tree; 49 50 /** 51 * Constructor. 52 * 53 * @param Tree $tree 54 */ 55 public function __construct(Tree $tree) 56 { 57 $this->tree = $tree; 58 } 59 60 /** 61 * General query on family. 62 * 63 * @param string $type 64 * 65 * @return string 66 */ 67 private function familyQuery(string $type): string 68 { 69 $row = DB::table('families') 70 ->where('f_file', '=', $this->tree->id()) 71 ->orderBy('f_numchil', 'desc') 72 ->first(); 73 74 if ($row === null) { 75 return ''; 76 } 77 78 /** @var Family $family */ 79 $family = Family::rowMapper()($row); 80 81 if (!$family->canShow()) { 82 return I18N::translate('This information is private and cannot be shown.'); 83 } 84 85 switch ($type) { 86 default: 87 case 'full': 88 return $family->formatList(); 89 90 case 'size': 91 return I18N::number((int) $row->f_numchil); 92 93 case 'name': 94 return '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 95 } 96 } 97 98 /** 99 * Find the family with the most children. 100 * 101 * @return string 102 */ 103 public function largestFamily(): string 104 { 105 return $this->familyQuery('full'); 106 } 107 108 /** 109 * Find the number of children in the largest family. 110 * 111 * @return string 112 */ 113 public function largestFamilySize(): string 114 { 115 return $this->familyQuery('size'); 116 } 117 118 /** 119 * Find the family with the most children. 120 * 121 * @return string 122 */ 123 public function largestFamilyName(): string 124 { 125 return $this->familyQuery('name'); 126 } 127 128 /** 129 * Find the couple with the most grandchildren. 130 * 131 * @param int $total 132 * 133 * @return array 134 */ 135 private function topTenGrandFamilyQuery(int $total): array 136 { 137 return DB::table('families') 138 ->join('link AS children', static function (JoinClause $join): void { 139 $join 140 ->on('children.l_from', '=', 'f_id') 141 ->on('children.l_file', '=', 'f_file') 142 ->where('children.l_type', '=', 'CHIL'); 143 })->join('link AS mchildren', static function (JoinClause $join): void { 144 $join 145 ->on('mchildren.l_file', '=', 'children.l_file') 146 ->on('mchildren.l_from', '=', 'children.l_to') 147 ->where('mchildren.l_type', '=', 'FAMS'); 148 })->join('link AS gchildren', static function (JoinClause $join): void { 149 $join 150 ->on('gchildren.l_file', '=', 'mchildren.l_file') 151 ->on('gchildren.l_from', '=', 'mchildren.l_to') 152 ->where('gchildren.l_type', '=', 'CHIL'); 153 }) 154 ->where('f_file', '=', $this->tree->id()) 155 ->groupBy(['f_id', 'f_file']) 156 ->orderBy(new Expression('COUNT(*)'), 'DESC') 157 ->select('families.*') 158 ->limit($total) 159 ->get() 160 ->map(Family::rowMapper()) 161 ->filter(GedcomRecord::accessFilter()) 162 ->map(static function (Family $family): array { 163 $count = 0; 164 foreach ($family->children() as $child) { 165 foreach ($child->spouseFamilies() as $spouse_family) { 166 $count += $spouse_family->children()->count(); 167 } 168 } 169 170 return [ 171 'family' => $family, 172 'count' => $count, 173 ]; 174 }) 175 ->all(); 176 } 177 178 /** 179 * Find the couple with the most grandchildren. 180 * 181 * @param int $total 182 * 183 * @return string 184 */ 185 public function topTenLargestGrandFamily(int $total = 10): string 186 { 187 return view('statistics/families/top10-nolist-grand', [ 188 'records' => $this->topTenGrandFamilyQuery($total), 189 ]); 190 } 191 192 /** 193 * Find the couple with the most grandchildren. 194 * 195 * @param int $total 196 * 197 * @return string 198 */ 199 public function topTenLargestGrandFamilyList(int $total = 10): string 200 { 201 return view('statistics/families/top10-list-grand', [ 202 'records' => $this->topTenGrandFamilyQuery($total), 203 ]); 204 } 205 206 /** 207 * Find the families with no children. 208 * 209 * @return int 210 */ 211 private function noChildrenFamiliesQuery(): int 212 { 213 return DB::table('families') 214 ->where('f_file', '=', $this->tree->id()) 215 ->where('f_numchil', '=', 0) 216 ->count(); 217 } 218 219 /** 220 * Find the families with no children. 221 * 222 * @return string 223 */ 224 public function noChildrenFamilies(): string 225 { 226 return I18N::number($this->noChildrenFamiliesQuery()); 227 } 228 229 /** 230 * Find the families with no children. 231 * 232 * @param string $type 233 * 234 * @return string 235 */ 236 public function noChildrenFamiliesList($type = 'list'): string 237 { 238 $families = DB::table('families') 239 ->where('f_file', '=', $this->tree->id()) 240 ->where('f_numchil', '=', 0) 241 ->get() 242 ->map(Family::rowMapper()) 243 ->filter(GedcomRecord::accessFilter()); 244 245 $top10 = []; 246 247 /** @var Family $family */ 248 foreach ($families as $family) { 249 if ($type === 'list') { 250 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>'; 251 } else { 252 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 253 } 254 } 255 256 if ($type === 'list') { 257 $top10 = implode('', $top10); 258 } else { 259 $top10 = implode('; ', $top10); 260 } 261 262 263 if ($type === 'list') { 264 return '<ul>' . $top10 . '</ul>'; 265 } 266 267 return $top10; 268 } 269 270 /** 271 * Create a chart of children with no families. 272 * 273 * @param int $year1 274 * @param int $year2 275 * 276 * @return string 277 */ 278 public function chartNoChildrenFamilies(int $year1 = -1, int $year2 = -1): string 279 { 280 $no_child_fam = $this->noChildrenFamiliesQuery(); 281 282 return (new ChartNoChildrenFamilies($this->tree)) 283 ->chartNoChildrenFamilies($no_child_fam, $year1, $year2); 284 } 285 286 /** 287 * Returns the ages between siblings. 288 * 289 * @param int $total The total number of records to query 290 * 291 * @return array 292 */ 293 private function ageBetweenSiblingsQuery(int $total): array 294 { 295 $prefix = DB::connection()->getTablePrefix(); 296 297 return DB::table('link AS link1') 298 ->join('link AS link2', static function (JoinClause $join): void { 299 $join 300 ->on('link2.l_from', '=', 'link1.l_from') 301 ->on('link2.l_type', '=', 'link1.l_type') 302 ->on('link2.l_file', '=', 'link1.l_file'); 303 }) 304 ->join('dates AS child1', static function (JoinClause $join): void { 305 $join 306 ->on('child1.d_gid', '=', 'link1.l_to') 307 ->on('child1.d_file', '=', 'link1.l_file') 308 ->where('child1.d_fact', '=', 'BIRT') 309 ->where('child1.d_julianday1', '<>', 0); 310 }) 311 ->join('dates AS child2', static function (JoinClause $join): void { 312 $join 313 ->on('child2.d_gid', '=', 'link2.l_to') 314 ->on('child2.d_file', '=', 'link2.l_file') 315 ->where('child2.d_fact', '=', 'BIRT') 316 ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1'); 317 }) 318 ->where('link1.l_type', '=', 'CHIL') 319 ->where('link1.l_file', '=', $this->tree->id()) 320 ->distinct() 321 ->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')]) 322 ->orderBy('age', 'DESC') 323 ->take($total) 324 ->get() 325 ->all(); 326 } 327 328 /** 329 * Returns the calculated age the time of event. 330 * 331 * @param int $age The age from the database record 332 * 333 * @return string 334 */ 335 private function calculateAge(int $age): string 336 { 337 if ((int) ($age / 365.25) > 0) { 338 $result = (int) ($age / 365.25) . 'y'; 339 } elseif ((int) ($age / 30.4375) > 0) { 340 $result = (int) ($age / 30.4375) . 'm'; 341 } else { 342 $result = $age . 'd'; 343 } 344 345 return FunctionsDate::getAgeAtEvent($result); 346 } 347 348 /** 349 * Find the ages between siblings. 350 * 351 * @param int $total The total number of records to query 352 * 353 * @return array 354 * @throws Exception 355 */ 356 private function ageBetweenSiblingsNoList(int $total): array 357 { 358 $rows = $this->ageBetweenSiblingsQuery($total); 359 360 foreach ($rows as $fam) { 361 $family = Family::getInstance($fam->family, $this->tree); 362 $child1 = Individual::getInstance($fam->ch1, $this->tree); 363 $child2 = Individual::getInstance($fam->ch2, $this->tree); 364 365 if ($child1->canShow() && $child2->canShow()) { 366 // ! Single array (no list) 367 return [ 368 'child1' => $child1, 369 'child2' => $child2, 370 'family' => $family, 371 'age' => $this->calculateAge((int) $fam->age), 372 ]; 373 } 374 } 375 376 return []; 377 } 378 379 /** 380 * Find the ages between siblings. 381 * 382 * @param int $total The total number of records to query 383 * @param bool $one Include each family only once if true 384 * 385 * @return array 386 * @throws Exception 387 */ 388 private function ageBetweenSiblingsList(int $total, bool $one): array 389 { 390 $rows = $this->ageBetweenSiblingsQuery($total); 391 $top10 = []; 392 $dist = []; 393 394 foreach ($rows as $fam) { 395 $family = Family::getInstance($fam->family, $this->tree); 396 $child1 = Individual::getInstance($fam->ch1, $this->tree); 397 $child2 = Individual::getInstance($fam->ch2, $this->tree); 398 399 $age = $this->calculateAge((int) $fam->age); 400 401 if ($one && !in_array($fam->family, $dist, true)) { 402 if ($child1->canShow() && $child2->canShow()) { 403 $top10[] = [ 404 'child1' => $child1, 405 'child2' => $child2, 406 'family' => $family, 407 'age' => $age, 408 ]; 409 410 $dist[] = $fam->family; 411 } 412 } elseif (!$one && $child1->canShow() && $child2->canShow()) { 413 $top10[] = [ 414 'child1' => $child1, 415 'child2' => $child2, 416 'family' => $family, 417 'age' => $age, 418 ]; 419 } 420 } 421 422 return $top10; 423 } 424 425 /** 426 * Find the ages between siblings. 427 * 428 * @param int $total The total number of records to query 429 * 430 * @return string 431 */ 432 private function ageBetweenSiblingsAge(int $total): string 433 { 434 $rows = $this->ageBetweenSiblingsQuery($total); 435 436 foreach ($rows as $fam) { 437 return $this->calculateAge((int) $fam->age); 438 } 439 440 return ''; 441 } 442 443 /** 444 * Find the ages between siblings. 445 * 446 * @param int $total The total number of records to query 447 * 448 * @return string 449 * @throws Exception 450 */ 451 private function ageBetweenSiblingsName(int $total): string 452 { 453 $rows = $this->ageBetweenSiblingsQuery($total); 454 455 foreach ($rows as $fam) { 456 $family = Family::getInstance($fam->family, $this->tree); 457 $child1 = Individual::getInstance($fam->ch1, $this->tree); 458 $child2 = Individual::getInstance($fam->ch2, $this->tree); 459 460 if ($child1->canShow() && $child2->canShow()) { 461 $return = '<a href="' . e($child2->url()) . '">' . $child2->fullName() . '</a> '; 462 $return .= I18N::translate('and') . ' '; 463 $return .= '<a href="' . e($child1->url()) . '">' . $child1->fullName() . '</a>'; 464 $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; 465 } else { 466 $return = I18N::translate('This information is private and cannot be shown.'); 467 } 468 469 return $return; 470 } 471 472 return ''; 473 } 474 475 /** 476 * Find the names of siblings with the widest age gap. 477 * 478 * @param int $total 479 * 480 * @return string 481 */ 482 public function topAgeBetweenSiblingsName(int $total = 10): string 483 { 484 return $this->ageBetweenSiblingsName($total); 485 } 486 487 /** 488 * Find the widest age gap between siblings. 489 * 490 * @param int $total 491 * 492 * @return string 493 */ 494 public function topAgeBetweenSiblings(int $total = 10): string 495 { 496 return $this->ageBetweenSiblingsAge($total); 497 } 498 499 /** 500 * Find the name of siblings with the widest age gap. 501 * 502 * @param int $total 503 * 504 * @return string 505 */ 506 public function topAgeBetweenSiblingsFullName(int $total = 10): string 507 { 508 $record = $this->ageBetweenSiblingsNoList($total); 509 510 if (empty($record)) { 511 return I18N::translate('This information is not available.'); 512 } 513 514 return view('statistics/families/top10-nolist-age', [ 515 'record' => $record, 516 ]); 517 } 518 519 /** 520 * Find the siblings with the widest age gaps. 521 * 522 * @param int $total 523 * @param string $one 524 * 525 * @return string 526 */ 527 public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string 528 { 529 $records = $this->ageBetweenSiblingsList($total, (bool) $one); 530 531 return view('statistics/families/top10-list-age', [ 532 'records' => $records, 533 ]); 534 } 535 536 /** 537 * General query on familes/children. 538 * 539 * @param int $year1 540 * @param int $year2 541 * 542 * @return stdClass[] 543 */ 544 public function statsChildrenQuery(int $year1 = -1, int $year2 = -1): array 545 { 546 $query = DB::table('families') 547 ->where('f_file', '=', $this->tree->id()) 548 ->groupBy('f_numchil') 549 ->select(['f_numchil', new Expression('COUNT(*) AS total')]); 550 551 if ($year1 >= 0 && $year2 >= 0) { 552 $query 553 ->join('dates', static function (JoinClause $join): void { 554 $join 555 ->on('d_file', '=', 'f_file') 556 ->on('d_gid', '=', 'f_id'); 557 }) 558 ->where('d_fact', '=', 'MARR') 559 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 560 ->whereBetween('d_year', [$year1, $year2]); 561 } 562 563 return $query->get()->all(); 564 } 565 566 /** 567 * Genearl query on families/children. 568 * 569 * @return string 570 */ 571 public function statsChildren(): string 572 { 573 return (new ChartChildren($this->tree)) 574 ->chartChildren(); 575 } 576 577 /** 578 * Count the total children. 579 * 580 * @return string 581 */ 582 public function totalChildren(): string 583 { 584 $total = (int) DB::table('families') 585 ->where('f_file', '=', $this->tree->id()) 586 ->sum('f_numchil'); 587 588 return I18N::number($total); 589 } 590 591 /** 592 * Find the average number of children in families. 593 * 594 * @return string 595 */ 596 public function averageChildren(): string 597 { 598 $average = (float) DB::table('families') 599 ->where('f_file', '=', $this->tree->id()) 600 ->avg('f_numchil'); 601 602 return I18N::number($average, 2); 603 } 604 605 /** 606 * General query on families. 607 * 608 * @param int $total 609 * 610 * @return array 611 */ 612 private function topTenFamilyQuery(int $total): array 613 { 614 return DB::table('families') 615 ->where('f_file', '=', $this->tree->id()) 616 ->orderBy('f_numchil', 'DESC') 617 ->limit($total) 618 ->get() 619 ->map(Family::rowMapper()) 620 ->filter(GedcomRecord::accessFilter()) 621 ->map(static function (Family $family): array { 622 return [ 623 'family' => $family, 624 'count' => $family->numberOfChildren(), 625 ]; 626 }) 627 ->all(); 628 } 629 630 /** 631 * The the families with the most children. 632 * 633 * @param int $total 634 * 635 * @return string 636 */ 637 public function topTenLargestFamily(int $total = 10): string 638 { 639 $records = $this->topTenFamilyQuery($total); 640 641 return view('statistics/families/top10-nolist', [ 642 'records' => $records, 643 ]); 644 } 645 646 /** 647 * Find the families with the most children. 648 * 649 * @param int $total 650 * 651 * @return string 652 */ 653 public function topTenLargestFamilyList(int $total = 10): string 654 { 655 $records = $this->topTenFamilyQuery($total); 656 657 return view('statistics/families/top10-list', [ 658 'records' => $records, 659 ]); 660 } 661 662 /** 663 * Create a chart of the largest families. 664 * 665 * @param string|null $color_from 666 * @param string|null $color_to 667 * @param int $total 668 * 669 * @return string 670 */ 671 public function chartLargestFamilies( 672 string $color_from = null, 673 string $color_to = null, 674 int $total = 10 675 ): string { 676 return (new ChartFamilyLargest($this->tree)) 677 ->chartLargestFamilies($color_from, $color_to, $total); 678 } 679 680 /** 681 * Find the month in the year of the birth of the first child. 682 * 683 * @param int $year1 684 * @param int $year2 685 * 686 * @return Builder 687 */ 688 public function monthFirstChildQuery(int $year1 = -1, int $year2 = -1): Builder 689 { 690 $first_child_subquery = DB::table('link') 691 ->join('dates', static function (JoinClause $join): void { 692 $join 693 ->on('d_gid', '=', 'l_to') 694 ->on('d_file', '=', 'l_file') 695 ->where('d_julianday1', '<>', 0) 696 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']); 697 }) 698 ->where('l_file', '=', $this->tree->id()) 699 ->where('l_type', '=', 'CHIL') 700 ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')]) 701 ->groupBy('family_id'); 702 703 $query = DB::table('link') 704 ->join('dates', static function (JoinClause $join): void { 705 $join 706 ->on('d_gid', '=', 'l_to') 707 ->on('d_file', '=', 'l_file'); 708 }) 709 ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void { 710 $join 711 ->on('family_id', '=', 'l_from') 712 ->on('min_birth_jd', '=', 'd_julianday1'); 713 }) 714 ->where('link.l_file', '=', $this->tree->id()) 715 ->where('link.l_type', '=', 'CHIL') 716 ->select(['d_month', new Expression('COUNT(*) AS total')]) 717 ->groupBy(['d_month']); 718 719 if ($year1 >= 0 && $year2 >= 0) { 720 $query->whereBetween('d_year', [$year1, $year2]); 721 } 722 723 return $query; 724 } 725 726 /** 727 * Find the month in the year of the birth of the first child. 728 * 729 * @param int $year1 730 * @param int $year2 731 * 732 * @return Builder 733 */ 734 public function monthFirstChildBySexQuery(int $year1 = -1, int $year2 = -1): Builder 735 { 736 return $this->monthFirstChildQuery($year1, $year2) 737 ->join('individuals', static function (JoinClause $join): void { 738 $join 739 ->on('i_file', '=', 'l_file') 740 ->on('i_id', '=', 'l_to'); 741 }) 742 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 743 ->groupBy(['d_month', 'i_sex']); 744 } 745 746 /** 747 * Number of husbands. 748 * 749 * @return string 750 */ 751 public function totalMarriedMales(): string 752 { 753 $n = (int) DB::table('families') 754 ->where('f_file', '=', $this->tree->id()) 755 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 756 ->distinct() 757 ->count('f_husb'); 758 759 return I18N::number($n); 760 } 761 762 /** 763 * Number of wives. 764 * 765 * @return string 766 */ 767 public function totalMarriedFemales(): string 768 { 769 $n = (int) DB::table('families') 770 ->where('f_file', '=', $this->tree->id()) 771 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 772 ->distinct() 773 ->count('f_wife'); 774 775 return I18N::number($n); 776 } 777 778 /** 779 * General query on parents. 780 * 781 * @param string $type 782 * @param string $age_dir 783 * @param string $sex 784 * @param bool $show_years 785 * 786 * @return string 787 */ 788 private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string 789 { 790 if ($sex === 'F') { 791 $sex_field = 'WIFE'; 792 } else { 793 $sex_field = 'HUSB'; 794 } 795 796 if ($age_dir !== 'ASC') { 797 $age_dir = 'DESC'; 798 } 799 800 $prefix = DB::connection()->getTablePrefix(); 801 802 $row = DB::table('link AS parentfamily') 803 ->join('link AS childfamily', static function (JoinClause $join): void { 804 $join 805 ->on('childfamily.l_file', '=', 'parentfamily.l_file') 806 ->on('childfamily.l_from', '=', 'parentfamily.l_from') 807 ->where('childfamily.l_type', '=', 'CHIL'); 808 }) 809 ->join('dates AS birth', static function (JoinClause $join): void { 810 $join 811 ->on('birth.d_file', '=', 'parentfamily.l_file') 812 ->on('birth.d_gid', '=', 'parentfamily.l_to') 813 ->where('birth.d_fact', '=', 'BIRT') 814 ->where('birth.d_julianday1', '<>', 0); 815 }) 816 ->join('dates AS childbirth', static function (JoinClause $join): void { 817 $join 818 ->on('childbirth.d_file', '=', 'parentfamily.l_file') 819 ->on('childbirth.d_gid', '=', 'childfamily.l_to') 820 ->where('childbirth.d_fact', '=', 'BIRT'); 821 }) 822 ->where('childfamily.l_file', '=', $this->tree->id()) 823 ->where('parentfamily.l_type', '=', $sex_field) 824 ->where('childbirth.d_julianday2', '>', 'birth.d_julianday1') 825 ->select(['parentfamily.l_to AS id', new Expression($prefix . 'childbirth.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]) 826 ->take(1) 827 ->orderBy('age', $age_dir) 828 ->get() 829 ->first(); 830 831 if ($row === null) { 832 return ''; 833 } 834 835 $person = Individual::getInstance($row->id, $this->tree); 836 837 switch ($type) { 838 default: 839 case 'full': 840 if ($person && $person->canShow()) { 841 $result = $person->formatList(); 842 } else { 843 $result = I18N::translate('This information is private and cannot be shown.'); 844 } 845 break; 846 847 case 'name': 848 $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>'; 849 break; 850 851 case 'age': 852 $age = $row->age; 853 854 if ($show_years) { 855 $result = $this->calculateAge((int) $row->age); 856 } else { 857 $result = (string) floor($age / 365.25); 858 } 859 860 break; 861 } 862 863 return $result; 864 } 865 866 /** 867 * Find the youngest mother 868 * 869 * @return string 870 */ 871 public function youngestMother(): string 872 { 873 return $this->parentsQuery('full', 'ASC', 'F', false); 874 } 875 876 /** 877 * Find the name of the youngest mother. 878 * 879 * @return string 880 */ 881 public function youngestMotherName(): string 882 { 883 return $this->parentsQuery('name', 'ASC', 'F', false); 884 } 885 886 /** 887 * Find the age of the youngest mother. 888 * 889 * @param string $show_years 890 * 891 * @return string 892 */ 893 public function youngestMotherAge(string $show_years = ''): string 894 { 895 return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); 896 } 897 898 /** 899 * Find the oldest mother. 900 * 901 * @return string 902 */ 903 public function oldestMother(): string 904 { 905 return $this->parentsQuery('full', 'DESC', 'F', false); 906 } 907 908 /** 909 * Find the name of the oldest mother. 910 * 911 * @return string 912 */ 913 public function oldestMotherName(): string 914 { 915 return $this->parentsQuery('name', 'DESC', 'F', false); 916 } 917 918 /** 919 * Find the age of the oldest mother. 920 * 921 * @param string $show_years 922 * 923 * @return string 924 */ 925 public function oldestMotherAge(string $show_years = ''): string 926 { 927 return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); 928 } 929 930 /** 931 * Find the youngest father. 932 * 933 * @return string 934 */ 935 public function youngestFather(): string 936 { 937 return $this->parentsQuery('full', 'ASC', 'M', false); 938 } 939 940 /** 941 * Find the name of the youngest father. 942 * 943 * @return string 944 */ 945 public function youngestFatherName(): string 946 { 947 return $this->parentsQuery('name', 'ASC', 'M', false); 948 } 949 950 /** 951 * Find the age of the youngest father. 952 * 953 * @param string $show_years 954 * 955 * @return string 956 */ 957 public function youngestFatherAge(string $show_years = ''): string 958 { 959 return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); 960 } 961 962 /** 963 * Find the oldest father. 964 * 965 * @return string 966 */ 967 public function oldestFather(): string 968 { 969 return $this->parentsQuery('full', 'DESC', 'M', false); 970 } 971 972 /** 973 * Find the name of the oldest father. 974 * 975 * @return string 976 */ 977 public function oldestFatherName(): string 978 { 979 return $this->parentsQuery('name', 'DESC', 'M', false); 980 } 981 982 /** 983 * Find the age of the oldest father. 984 * 985 * @param string $show_years 986 * 987 * @return string 988 */ 989 public function oldestFatherAge(string $show_years = ''): string 990 { 991 return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); 992 } 993 994 /** 995 * General query on age at marriage. 996 * 997 * @param string $type 998 * @param string $age_dir "ASC" or "DESC" 999 * @param int $total 1000 * 1001 * @return string 1002 */ 1003 private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string 1004 { 1005 $prefix = DB::connection()->getTablePrefix(); 1006 1007 $hrows = DB::table('families') 1008 ->where('f_file', '=', $this->tree->id()) 1009 ->join('dates AS married', static function (JoinClause $join): void { 1010 $join 1011 ->on('married.d_file', '=', 'f_file') 1012 ->on('married.d_gid', '=', 'f_id') 1013 ->where('married.d_fact', '=', 'MARR') 1014 ->where('married.d_julianday1', '<>', 0); 1015 }) 1016 ->join('dates AS husbdeath', static function (JoinClause $join): void { 1017 $join 1018 ->on('husbdeath.d_gid', '=', 'f_husb') 1019 ->on('husbdeath.d_file', '=', 'f_file') 1020 ->where('husbdeath.d_fact', '=', 'DEAT'); 1021 }) 1022 ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2') 1023 ->groupBy('f_id') 1024 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'husbdeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1025 ->get() 1026 ->all(); 1027 1028 $wrows = DB::table('families') 1029 ->where('f_file', '=', $this->tree->id()) 1030 ->join('dates AS married', static function (JoinClause $join): void { 1031 $join 1032 ->on('married.d_file', '=', 'f_file') 1033 ->on('married.d_gid', '=', 'f_id') 1034 ->where('married.d_fact', '=', 'MARR') 1035 ->where('married.d_julianday1', '<>', 0); 1036 }) 1037 ->join('dates AS wifedeath', static function (JoinClause $join): void { 1038 $join 1039 ->on('wifedeath.d_gid', '=', 'f_wife') 1040 ->on('wifedeath.d_file', '=', 'f_file') 1041 ->where('wifedeath.d_fact', '=', 'DEAT'); 1042 }) 1043 ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2') 1044 ->groupBy('f_id') 1045 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'wifedeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1046 ->get() 1047 ->all(); 1048 1049 $drows = DB::table('families') 1050 ->where('f_file', '=', $this->tree->id()) 1051 ->join('dates AS married', static function (JoinClause $join): void { 1052 $join 1053 ->on('married.d_file', '=', 'f_file') 1054 ->on('married.d_gid', '=', 'f_id') 1055 ->where('married.d_fact', '=', 'MARR') 1056 ->where('married.d_julianday1', '<>', 0); 1057 }) 1058 ->join('dates AS divorced', static function (JoinClause $join): void { 1059 $join 1060 ->on('divorced.d_gid', '=', 'f_id') 1061 ->on('divorced.d_file', '=', 'f_file') 1062 ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']); 1063 }) 1064 ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2') 1065 ->groupBy('f_id') 1066 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'divorced.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1067 ->get() 1068 ->all(); 1069 1070 $rows = []; 1071 foreach ($drows as $family) { 1072 $rows[$family->family] = $family->age; 1073 } 1074 1075 foreach ($hrows as $family) { 1076 if (!isset($rows[$family->family])) { 1077 $rows[$family->family] = $family->age; 1078 } 1079 } 1080 1081 foreach ($wrows as $family) { 1082 if (!isset($rows[$family->family])) { 1083 $rows[$family->family] = $family->age; 1084 } elseif ($rows[$family->family] > $family->age) { 1085 $rows[$family->family] = $family->age; 1086 } 1087 } 1088 1089 if ($age_dir === 'DESC') { 1090 arsort($rows); 1091 } else { 1092 asort($rows); 1093 } 1094 1095 $top10 = []; 1096 $i = 0; 1097 foreach ($rows as $xref => $age) { 1098 $family = Family::getInstance((string) $xref, $this->tree); 1099 if ($type === 'name') { 1100 return $family->formatList(); 1101 } 1102 1103 $age = $this->calculateAge((int) $age); 1104 1105 if ($type === 'age') { 1106 return $age; 1107 } 1108 1109 $husb = $family->husband(); 1110 $wife = $family->wife(); 1111 1112 if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) 1113 && ($wife && ($wife->getAllDeathDates() || !$wife->isDead())) 1114 ) { 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