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