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 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Statistics\Repository; 21 22use Exception; 23use Fisharebest\Webtrees\Family; 24use Fisharebest\Webtrees\Functions\FunctionsDate; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Individual; 28use Fisharebest\Webtrees\Statistics\Google\ChartChildren; 29use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; 30use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; 31use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; 32use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; 33use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; 34use Fisharebest\Webtrees\Tree; 35use Illuminate\Database\Capsule\Manager as DB; 36use Illuminate\Database\Query\Builder; 37use Illuminate\Database\Query\Expression; 38use Illuminate\Database\Query\JoinClause; 39use stdClass; 40 41use function in_array; 42 43/** 44 * 45 */ 46class FamilyRepository 47{ 48 /** 49 * @var Tree 50 */ 51 private $tree; 52 53 /** 54 * Constructor. 55 * 56 * @param Tree $tree 57 */ 58 public function __construct(Tree $tree) 59 { 60 $this->tree = $tree; 61 } 62 63 /** 64 * General query on family. 65 * 66 * @param string $type 67 * 68 * @return string 69 */ 70 private function familyQuery(string $type): string 71 { 72 $row = DB::table('families') 73 ->where('f_file', '=', $this->tree->id()) 74 ->orderBy('f_numchil', 'desc') 75 ->first(); 76 77 if ($row === null) { 78 return ''; 79 } 80 81 /** @var Family $family */ 82 $family = Family::rowMapper($this->tree)($row); 83 84 if (!$family->canShow()) { 85 return I18N::translate('This information is private and cannot be shown.'); 86 } 87 88 switch ($type) { 89 default: 90 case 'full': 91 return $family->formatList(); 92 93 case 'size': 94 return I18N::number((int) $row->f_numchil); 95 96 case 'name': 97 return '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 98 } 99 } 100 101 /** 102 * Find the family with the most children. 103 * 104 * @return string 105 */ 106 public function largestFamily(): string 107 { 108 return $this->familyQuery('full'); 109 } 110 111 /** 112 * Find the number of children in the largest family. 113 * 114 * @return string 115 */ 116 public function largestFamilySize(): string 117 { 118 return $this->familyQuery('size'); 119 } 120 121 /** 122 * Find the family with the most children. 123 * 124 * @return string 125 */ 126 public function largestFamilyName(): string 127 { 128 return $this->familyQuery('name'); 129 } 130 131 /** 132 * Find the couple with the most grandchildren. 133 * 134 * @param int $total 135 * 136 * @return array 137 */ 138 private function topTenGrandFamilyQuery(int $total): array 139 { 140 return DB::table('families') 141 ->join('link AS children', static function (JoinClause $join): void { 142 $join 143 ->on('children.l_from', '=', 'f_id') 144 ->on('children.l_file', '=', 'f_file') 145 ->where('children.l_type', '=', 'CHIL'); 146 })->join('link AS mchildren', static function (JoinClause $join): void { 147 $join 148 ->on('mchildren.l_file', '=', 'children.l_file') 149 ->on('mchildren.l_from', '=', 'children.l_to') 150 ->where('mchildren.l_type', '=', 'FAMS'); 151 })->join('link AS gchildren', static function (JoinClause $join): void { 152 $join 153 ->on('gchildren.l_file', '=', 'mchildren.l_file') 154 ->on('gchildren.l_from', '=', 'mchildren.l_to') 155 ->where('gchildren.l_type', '=', 'CHIL'); 156 }) 157 ->where('f_file', '=', $this->tree->id()) 158 ->groupBy(['f_id', 'f_file']) 159 ->orderBy(new Expression('COUNT(*)'), 'DESC') 160 ->select(['families.*']) 161 ->limit($total) 162 ->get() 163 ->map(Family::rowMapper($this->tree)) 164 ->filter(GedcomRecord::accessFilter()) 165 ->map(static function (Family $family): array { 166 $count = 0; 167 foreach ($family->children() as $child) { 168 foreach ($child->spouseFamilies() as $spouse_family) { 169 $count += $spouse_family->children()->count(); 170 } 171 } 172 173 return [ 174 'family' => $family, 175 'count' => $count, 176 ]; 177 }) 178 ->all(); 179 } 180 181 /** 182 * Find the couple with the most grandchildren. 183 * 184 * @param int $total 185 * 186 * @return string 187 */ 188 public function topTenLargestGrandFamily(int $total = 10): string 189 { 190 return view('statistics/families/top10-nolist-grand', [ 191 'records' => $this->topTenGrandFamilyQuery($total), 192 ]); 193 } 194 195 /** 196 * Find the couple with the most grandchildren. 197 * 198 * @param int $total 199 * 200 * @return string 201 */ 202 public function topTenLargestGrandFamilyList(int $total = 10): string 203 { 204 return view('statistics/families/top10-list-grand', [ 205 'records' => $this->topTenGrandFamilyQuery($total), 206 ]); 207 } 208 209 /** 210 * Find the families with no children. 211 * 212 * @return int 213 */ 214 private function noChildrenFamiliesQuery(): int 215 { 216 return DB::table('families') 217 ->where('f_file', '=', $this->tree->id()) 218 ->where('f_numchil', '=', 0) 219 ->count(); 220 } 221 222 /** 223 * Find the families with no children. 224 * 225 * @return string 226 */ 227 public function noChildrenFamilies(): string 228 { 229 return I18N::number($this->noChildrenFamiliesQuery()); 230 } 231 232 /** 233 * Find the families with no children. 234 * 235 * @param string $type 236 * 237 * @return string 238 */ 239 public function noChildrenFamiliesList($type = 'list'): string 240 { 241 $families = DB::table('families') 242 ->where('f_file', '=', $this->tree->id()) 243 ->where('f_numchil', '=', 0) 244 ->get() 245 ->map(Family::rowMapper($this->tree)) 246 ->filter(GedcomRecord::accessFilter()); 247 248 $top10 = []; 249 250 /** @var Family $family */ 251 foreach ($families as $family) { 252 if ($type === 'list') { 253 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>'; 254 } else { 255 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 256 } 257 } 258 259 if ($type === 'list') { 260 $top10 = implode('', $top10); 261 } else { 262 $top10 = implode('; ', $top10); 263 } 264 265 266 if ($type === 'list') { 267 return '<ul>' . $top10 . '</ul>'; 268 } 269 270 return $top10; 271 } 272 273 /** 274 * Create a chart of children with no families. 275 * 276 * @param int $year1 277 * @param int $year2 278 * 279 * @return string 280 */ 281 public function chartNoChildrenFamilies(int $year1 = -1, int $year2 = -1): string 282 { 283 $no_child_fam = $this->noChildrenFamiliesQuery(); 284 285 return (new ChartNoChildrenFamilies($this->tree)) 286 ->chartNoChildrenFamilies($no_child_fam, $year1, $year2); 287 } 288 289 /** 290 * Returns the ages between siblings. 291 * 292 * @param int $total The total number of records to query 293 * 294 * @return array 295 */ 296 private function ageBetweenSiblingsQuery(int $total): array 297 { 298 $prefix = DB::connection()->getTablePrefix(); 299 300 return DB::table('link AS link1') 301 ->join('link AS link2', static function (JoinClause $join): void { 302 $join 303 ->on('link2.l_from', '=', 'link1.l_from') 304 ->on('link2.l_type', '=', 'link1.l_type') 305 ->on('link2.l_file', '=', 'link1.l_file'); 306 }) 307 ->join('dates AS child1', static function (JoinClause $join): void { 308 $join 309 ->on('child1.d_gid', '=', 'link1.l_to') 310 ->on('child1.d_file', '=', 'link1.l_file') 311 ->where('child1.d_fact', '=', 'BIRT') 312 ->where('child1.d_julianday1', '<>', 0); 313 }) 314 ->join('dates AS child2', static function (JoinClause $join): void { 315 $join 316 ->on('child2.d_gid', '=', 'link2.l_to') 317 ->on('child2.d_file', '=', 'link2.l_file') 318 ->where('child2.d_fact', '=', 'BIRT') 319 ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1'); 320 }) 321 ->where('link1.l_type', '=', 'CHIL') 322 ->where('link1.l_file', '=', $this->tree->id()) 323 ->distinct() 324 ->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')]) 325 ->orderBy('age', 'DESC') 326 ->take($total) 327 ->get() 328 ->all(); 329 } 330 331 /** 332 * Returns the calculated age the time of event. 333 * 334 * @param int $age The age from the database record 335 * 336 * @return string 337 */ 338 private function calculateAge(int $age): string 339 { 340 if ((int) ($age / 365.25) > 0) { 341 $result = (int) ($age / 365.25) . 'y'; 342 } elseif ((int) ($age / 30.4375) > 0) { 343 $result = (int) ($age / 30.4375) . 'm'; 344 } else { 345 $result = $age . 'd'; 346 } 347 348 return FunctionsDate::getAgeAtEvent($result); 349 } 350 351 /** 352 * Find the ages between siblings. 353 * 354 * @param int $total The total number of records to query 355 * 356 * @return array 357 * @throws Exception 358 */ 359 private function ageBetweenSiblingsNoList(int $total): array 360 { 361 $rows = $this->ageBetweenSiblingsQuery($total); 362 363 foreach ($rows as $fam) { 364 $family = Family::getInstance($fam->family, $this->tree); 365 $child1 = Individual::getInstance($fam->ch1, $this->tree); 366 $child2 = Individual::getInstance($fam->ch2, $this->tree); 367 368 if ($child1->canShow() && $child2->canShow()) { 369 // ! Single array (no list) 370 return [ 371 'child1' => $child1, 372 'child2' => $child2, 373 'family' => $family, 374 'age' => $this->calculateAge((int) $fam->age), 375 ]; 376 } 377 } 378 379 return []; 380 } 381 382 /** 383 * Find the ages between siblings. 384 * 385 * @param int $total The total number of records to query 386 * @param bool $one Include each family only once if true 387 * 388 * @return array 389 * @throws Exception 390 */ 391 private function ageBetweenSiblingsList(int $total, bool $one): array 392 { 393 $rows = $this->ageBetweenSiblingsQuery($total); 394 $top10 = []; 395 $dist = []; 396 397 foreach ($rows as $fam) { 398 $family = Family::getInstance($fam->family, $this->tree); 399 $child1 = Individual::getInstance($fam->ch1, $this->tree); 400 $child2 = Individual::getInstance($fam->ch2, $this->tree); 401 402 $age = $this->calculateAge((int) $fam->age); 403 404 if ($one && !in_array($fam->family, $dist, true)) { 405 if ($child1->canShow() && $child2->canShow()) { 406 $top10[] = [ 407 'child1' => $child1, 408 'child2' => $child2, 409 'family' => $family, 410 'age' => $age, 411 ]; 412 413 $dist[] = $fam->family; 414 } 415 } elseif (!$one && $child1->canShow() && $child2->canShow()) { 416 $top10[] = [ 417 'child1' => $child1, 418 'child2' => $child2, 419 'family' => $family, 420 'age' => $age, 421 ]; 422 } 423 } 424 425 return $top10; 426 } 427 428 /** 429 * Find the ages between siblings. 430 * 431 * @param int $total The total number of records to query 432 * 433 * @return string 434 */ 435 private function ageBetweenSiblingsAge(int $total): string 436 { 437 $rows = $this->ageBetweenSiblingsQuery($total); 438 439 foreach ($rows as $fam) { 440 return $this->calculateAge((int) $fam->age); 441 } 442 443 return ''; 444 } 445 446 /** 447 * Find the ages between siblings. 448 * 449 * @param int $total The total number of records to query 450 * 451 * @return string 452 * @throws Exception 453 */ 454 private function ageBetweenSiblingsName(int $total): string 455 { 456 $rows = $this->ageBetweenSiblingsQuery($total); 457 458 foreach ($rows as $fam) { 459 $family = Family::getInstance($fam->family, $this->tree); 460 $child1 = Individual::getInstance($fam->ch1, $this->tree); 461 $child2 = Individual::getInstance($fam->ch2, $this->tree); 462 463 if ($child1->canShow() && $child2->canShow()) { 464 $return = '<a href="' . e($child2->url()) . '">' . $child2->fullName() . '</a> '; 465 $return .= I18N::translate('and') . ' '; 466 $return .= '<a href="' . e($child1->url()) . '">' . $child1->fullName() . '</a>'; 467 $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; 468 } else { 469 $return = I18N::translate('This information is private and cannot be shown.'); 470 } 471 472 return $return; 473 } 474 475 return ''; 476 } 477 478 /** 479 * Find the names of siblings with the widest age gap. 480 * 481 * @param int $total 482 * 483 * @return string 484 */ 485 public function topAgeBetweenSiblingsName(int $total = 10): string 486 { 487 return $this->ageBetweenSiblingsName($total); 488 } 489 490 /** 491 * Find the widest age gap between siblings. 492 * 493 * @param int $total 494 * 495 * @return string 496 */ 497 public function topAgeBetweenSiblings(int $total = 10): string 498 { 499 return $this->ageBetweenSiblingsAge($total); 500 } 501 502 /** 503 * Find the name of siblings with the widest age gap. 504 * 505 * @param int $total 506 * 507 * @return string 508 */ 509 public function topAgeBetweenSiblingsFullName(int $total = 10): string 510 { 511 $record = $this->ageBetweenSiblingsNoList($total); 512 513 if ($record === []) { 514 return I18N::translate('This information is not available.'); 515 } 516 517 return view('statistics/families/top10-nolist-age', [ 518 'record' => $record, 519 ]); 520 } 521 522 /** 523 * Find the siblings with the widest age gaps. 524 * 525 * @param int $total 526 * @param string $one 527 * 528 * @return string 529 */ 530 public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string 531 { 532 $records = $this->ageBetweenSiblingsList($total, (bool) $one); 533 534 return view('statistics/families/top10-list-age', [ 535 'records' => $records, 536 ]); 537 } 538 539 /** 540 * General query on familes/children. 541 * 542 * @param int $year1 543 * @param int $year2 544 * 545 * @return stdClass[] 546 */ 547 public function statsChildrenQuery(int $year1 = -1, int $year2 = -1): array 548 { 549 $query = DB::table('families') 550 ->where('f_file', '=', $this->tree->id()) 551 ->groupBy(['f_numchil']) 552 ->select(['f_numchil', new Expression('COUNT(*) AS total')]); 553 554 if ($year1 >= 0 && $year2 >= 0) { 555 $query 556 ->join('dates', static function (JoinClause $join): void { 557 $join 558 ->on('d_file', '=', 'f_file') 559 ->on('d_gid', '=', 'f_id'); 560 }) 561 ->where('d_fact', '=', 'MARR') 562 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 563 ->whereBetween('d_year', [$year1, $year2]); 564 } 565 566 return $query->get()->all(); 567 } 568 569 /** 570 * Genearl query on families/children. 571 * 572 * @return string 573 */ 574 public function statsChildren(): string 575 { 576 return (new ChartChildren($this->tree)) 577 ->chartChildren(); 578 } 579 580 /** 581 * Count the total children. 582 * 583 * @return string 584 */ 585 public function totalChildren(): string 586 { 587 $total = (int) DB::table('families') 588 ->where('f_file', '=', $this->tree->id()) 589 ->sum('f_numchil'); 590 591 return I18N::number($total); 592 } 593 594 /** 595 * Find the average number of children in families. 596 * 597 * @return string 598 */ 599 public function averageChildren(): string 600 { 601 $average = (float) DB::table('families') 602 ->where('f_file', '=', $this->tree->id()) 603 ->avg('f_numchil'); 604 605 return I18N::number($average, 2); 606 } 607 608 /** 609 * General query on families. 610 * 611 * @param int $total 612 * 613 * @return array 614 */ 615 private function topTenFamilyQuery(int $total): array 616 { 617 return DB::table('families') 618 ->where('f_file', '=', $this->tree->id()) 619 ->orderBy('f_numchil', 'DESC') 620 ->limit($total) 621 ->get() 622 ->map(Family::rowMapper($this->tree)) 623 ->filter(GedcomRecord::accessFilter()) 624 ->map(static function (Family $family): array { 625 return [ 626 'family' => $family, 627 'count' => $family->numberOfChildren(), 628 ]; 629 }) 630 ->all(); 631 } 632 633 /** 634 * The the families with the most children. 635 * 636 * @param int $total 637 * 638 * @return string 639 */ 640 public function topTenLargestFamily(int $total = 10): string 641 { 642 $records = $this->topTenFamilyQuery($total); 643 644 return view('statistics/families/top10-nolist', [ 645 'records' => $records, 646 ]); 647 } 648 649 /** 650 * Find the families with the most children. 651 * 652 * @param int $total 653 * 654 * @return string 655 */ 656 public function topTenLargestFamilyList(int $total = 10): string 657 { 658 $records = $this->topTenFamilyQuery($total); 659 660 return view('statistics/families/top10-list', [ 661 'records' => $records, 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', static 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', new Expression('MIN(d_julianday1) AS min_birth_jd')]) 704 ->groupBy(['family_id']); 705 706 $query = DB::table('link') 707 ->join('dates', static 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', static 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', new Expression('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', static 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', new Expression('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 = 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 = 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', static 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', static 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', static 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', new Expression($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', static 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', static 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', new Expression('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', static 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', static 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', new Expression('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', static 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', static 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', new Expression('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 $xref => $age) { 1101 $family = Family::getInstance((string) $xref, $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())) && ($wife && ($wife->getAllDeathDates() || !$wife->isDead()))) { 1116 if ($family && $family->canShow()) { 1117 if ($type === 'list') { 1118 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>'; 1119 } else { 1120 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')'; 1121 } 1122 } 1123 if (++$i === $total) { 1124 break; 1125 } 1126 } 1127 } 1128 1129 if ($type === 'list') { 1130 $top10 = implode('', $top10); 1131 } else { 1132 $top10 = implode('; ', $top10); 1133 } 1134 1135 if (I18N::direction() === 'rtl') { 1136 $top10 = str_replace([ 1137 '[', 1138 ']', 1139 '(', 1140 ')', 1141 '+', 1142 ], [ 1143 '‏[', 1144 '‏]', 1145 '‏(', 1146 '‏)', 1147 '‏+', 1148 ], $top10); 1149 } 1150 1151 if ($type === 'list') { 1152 return '<ul>' . $top10 . '</ul>'; 1153 } 1154 1155 return $top10; 1156 } 1157 1158 /** 1159 * General query on marriage ages. 1160 * 1161 * @return string 1162 */ 1163 public function topAgeOfMarriageFamily(): string 1164 { 1165 return $this->ageOfMarriageQuery('name', 'DESC', 1); 1166 } 1167 1168 /** 1169 * General query on marriage ages. 1170 * 1171 * @return string 1172 */ 1173 public function topAgeOfMarriage(): string 1174 { 1175 return $this->ageOfMarriageQuery('age', 'DESC', 1); 1176 } 1177 1178 /** 1179 * General query on marriage ages. 1180 * 1181 * @param int $total 1182 * 1183 * @return string 1184 */ 1185 public function topAgeOfMarriageFamilies(int $total = 10): string 1186 { 1187 return $this->ageOfMarriageQuery('nolist', 'DESC', $total); 1188 } 1189 1190 /** 1191 * General query on marriage ages. 1192 * 1193 * @param int $total 1194 * 1195 * @return string 1196 */ 1197 public function topAgeOfMarriageFamiliesList(int $total = 10): string 1198 { 1199 return $this->ageOfMarriageQuery('list', 'DESC', $total); 1200 } 1201 1202 /** 1203 * General query on marriage ages. 1204 * 1205 * @return string 1206 */ 1207 public function minAgeOfMarriageFamily(): string 1208 { 1209 return $this->ageOfMarriageQuery('name', 'ASC', 1); 1210 } 1211 1212 /** 1213 * General query on marriage ages. 1214 * 1215 * @return string 1216 */ 1217 public function minAgeOfMarriage(): string 1218 { 1219 return $this->ageOfMarriageQuery('age', 'ASC', 1); 1220 } 1221 1222 /** 1223 * General query on marriage ages. 1224 * 1225 * @param int $total 1226 * 1227 * @return string 1228 */ 1229 public function minAgeOfMarriageFamilies(int $total = 10): string 1230 { 1231 return $this->ageOfMarriageQuery('nolist', 'ASC', $total); 1232 } 1233 1234 /** 1235 * General query on marriage ages. 1236 * 1237 * @param int $total 1238 * 1239 * @return string 1240 */ 1241 public function minAgeOfMarriageFamiliesList(int $total = 10): string 1242 { 1243 return $this->ageOfMarriageQuery('list', 'ASC', $total); 1244 } 1245 1246 /** 1247 * Find the ages between spouses. 1248 * 1249 * @param string $age_dir 1250 * @param int $total 1251 * 1252 * @return array 1253 */ 1254 private function ageBetweenSpousesQuery(string $age_dir, int $total): array 1255 { 1256 $prefix = DB::connection()->getTablePrefix(); 1257 1258 $query = DB::table('families') 1259 ->where('f_file', '=', $this->tree->id()) 1260 ->join('dates AS wife', static function (JoinClause $join): void { 1261 $join 1262 ->on('wife.d_gid', '=', 'f_wife') 1263 ->on('wife.d_file', '=', 'f_file') 1264 ->where('wife.d_fact', '=', 'BIRT') 1265 ->where('wife.d_julianday1', '<>', 0); 1266 }) 1267 ->join('dates AS husb', static function (JoinClause $join): void { 1268 $join 1269 ->on('husb.d_gid', '=', 'f_husb') 1270 ->on('husb.d_file', '=', 'f_file') 1271 ->where('husb.d_fact', '=', 'BIRT') 1272 ->where('husb.d_julianday1', '<>', 0); 1273 }); 1274 1275 if ($age_dir === 'DESC') { 1276 $query 1277 ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1') 1278 ->orderBy(new Expression('MIN(' . $prefix . 'wife.d_julianday1) - MIN(' . $prefix . 'husb.d_julianday1)'), 'DESC'); 1279 } else { 1280 $query 1281 ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1') 1282 ->orderBy(new Expression('MIN(' . $prefix . 'husb.d_julianday1) - MIN(' . $prefix . 'wife.d_julianday1)'), 'DESC'); 1283 } 1284 1285 return $query 1286 ->groupBy(['f_id', 'f_file']) 1287 ->select(['families.*']) 1288 ->take($total) 1289 ->get() 1290 ->map(Family::rowMapper($this->tree)) 1291 ->filter(GedcomRecord::accessFilter()) 1292 ->map(function (Family $family) use ($age_dir): array { 1293 $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay(); 1294 $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay(); 1295 1296 if ($age_dir === 'DESC') { 1297 $diff = $wife_birt_jd - $husb_birt_jd; 1298 } else { 1299 $diff = $husb_birt_jd - $wife_birt_jd; 1300 } 1301 1302 return [ 1303 'family' => $family, 1304 'age' => $this->calculateAge($diff), 1305 ]; 1306 }) 1307 ->all(); 1308 } 1309 1310 /** 1311 * Find the age between husband and wife. 1312 * 1313 * @param int $total 1314 * 1315 * @return string 1316 */ 1317 public function ageBetweenSpousesMF(int $total = 10): string 1318 { 1319 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1320 1321 return view('statistics/families/top10-nolist-spouses', [ 1322 'records' => $records, 1323 ]); 1324 } 1325 1326 /** 1327 * Find the age between husband and wife. 1328 * 1329 * @param int $total 1330 * 1331 * @return string 1332 */ 1333 public function ageBetweenSpousesMFList(int $total = 10): string 1334 { 1335 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1336 1337 return view('statistics/families/top10-list-spouses', [ 1338 'records' => $records, 1339 ]); 1340 } 1341 1342 /** 1343 * Find the age between wife and husband.. 1344 * 1345 * @param int $total 1346 * 1347 * @return string 1348 */ 1349 public function ageBetweenSpousesFM(int $total = 10): string 1350 { 1351 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1352 1353 return view('statistics/families/top10-nolist-spouses', [ 1354 'records' => $records, 1355 ]); 1356 } 1357 1358 /** 1359 * Find the age between wife and husband.. 1360 * 1361 * @param int $total 1362 * 1363 * @return string 1364 */ 1365 public function ageBetweenSpousesFMList(int $total = 10): string 1366 { 1367 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1368 1369 return view('statistics/families/top10-list-spouses', [ 1370 'records' => $records, 1371 ]); 1372 } 1373 1374 /** 1375 * General query on ages at marriage. 1376 * 1377 * @param string $sex "M" or "F" 1378 * @param int $year1 1379 * @param int $year2 1380 * 1381 * @return array 1382 */ 1383 public function statsMarrAgeQuery($sex, $year1 = -1, $year2 = -1): array 1384 { 1385 $prefix = DB::connection()->getTablePrefix(); 1386 1387 $query = DB::table('dates AS married') 1388 ->join('families', static function (JoinClause $join): void { 1389 $join 1390 ->on('f_file', '=', 'married.d_file') 1391 ->on('f_id', '=', 'married.d_gid'); 1392 }) 1393 ->join('dates AS birth', static function (JoinClause $join) use ($sex): void { 1394 $join 1395 ->on('birth.d_file', '=', 'married.d_file') 1396 ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife') 1397 ->where('birth.d_julianday1', '<>', 0) 1398 ->where('birth.d_fact', '=', 'BIRT') 1399 ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']); 1400 }) 1401 ->where('married.d_file', '=', $this->tree->id()) 1402 ->where('married.d_fact', '=', 'MARR') 1403 ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 1404 ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1') 1405 ->select(['f_id', 'birth.d_gid', new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]); 1406 1407 if ($year1 >= 0 && $year2 >= 0) { 1408 $query->whereBetween('married.d_year', [$year1, $year2]); 1409 } 1410 1411 return $query 1412 ->get() 1413 ->map(static function (stdClass $row): stdClass { 1414 $row->age = (int) $row->age; 1415 1416 return $row; 1417 }) 1418 ->all(); 1419 } 1420 1421 /** 1422 * General query on marriage ages. 1423 * 1424 * @return string 1425 */ 1426 public function statsMarrAge(): string 1427 { 1428 return (new ChartMarriageAge($this->tree)) 1429 ->chartMarriageAge(); 1430 } 1431 1432 /** 1433 * Query the database for marriage tags. 1434 * 1435 * @param string $type "full", "name" or "age" 1436 * @param string $age_dir "ASC" or "DESC" 1437 * @param string $sex "F" or "M" 1438 * @param bool $show_years 1439 * 1440 * @return string 1441 */ 1442 private function marriageQuery(string $type, string $age_dir, string $sex, bool $show_years): string 1443 { 1444 if ($sex === 'F') { 1445 $sex_field = 'f_wife'; 1446 } else { 1447 $sex_field = 'f_husb'; 1448 } 1449 1450 if ($age_dir !== 'ASC') { 1451 $age_dir = 'DESC'; 1452 } 1453 1454 $prefix = DB::connection()->getTablePrefix(); 1455 1456 $row = DB::table('families') 1457 ->join('dates AS married', static function (JoinClause $join): void { 1458 $join 1459 ->on('married.d_file', '=', 'f_file') 1460 ->on('married.d_gid', '=', 'f_id') 1461 ->where('married.d_fact', '=', 'MARR'); 1462 }) 1463 ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void { 1464 $join 1465 ->on('i_file', '=', 'f_file') 1466 ->on('i_id', '=', $sex_field) 1467 ->where('i_sex', '=', $sex); 1468 }) 1469 ->join('dates AS birth', static function (JoinClause $join): void { 1470 $join 1471 ->on('birth.d_file', '=', 'i_file') 1472 ->on('birth.d_gid', '=', 'i_id') 1473 ->where('birth.d_fact', '=', 'BIRT') 1474 ->where('birth.d_julianday1', '<>', 0); 1475 }) 1476 ->where('f_file', '=', $this->tree->id()) 1477 ->where('married.d_julianday2', '>', 'birth.d_julianday1') 1478 ->orderBy(new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1'), $age_dir) 1479 ->select(['f_id AS famid', $sex_field, new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age'), 'i_id']) 1480 ->take(1) 1481 ->get() 1482 ->first(); 1483 1484 if ($row === null) { 1485 return ''; 1486 } 1487 1488 $family = Family::getInstance($row->famid, $this->tree); 1489 $person = Individual::getInstance($row->i_id, $this->tree); 1490 1491 switch ($type) { 1492 default: 1493 case 'full': 1494 if ($family && $family->canShow()) { 1495 $result = $family->formatList(); 1496 } else { 1497 $result = I18N::translate('This information is private and cannot be shown.'); 1498 } 1499 break; 1500 1501 case 'name': 1502 $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>'; 1503 break; 1504 1505 case 'age': 1506 $age = $row->age; 1507 1508 if ($show_years) { 1509 $result = $this->calculateAge((int) $row->age); 1510 } else { 1511 $result = I18N::number((int) ($age / 365.25)); 1512 } 1513 1514 break; 1515 } 1516 1517 return $result; 1518 } 1519 1520 /** 1521 * Find the youngest wife. 1522 * 1523 * @return string 1524 */ 1525 public function youngestMarriageFemale(): string 1526 { 1527 return $this->marriageQuery('full', 'ASC', 'F', false); 1528 } 1529 1530 /** 1531 * Find the name of the youngest wife. 1532 * 1533 * @return string 1534 */ 1535 public function youngestMarriageFemaleName(): string 1536 { 1537 return $this->marriageQuery('name', 'ASC', 'F', false); 1538 } 1539 1540 /** 1541 * Find the age of the youngest wife. 1542 * 1543 * @param string $show_years 1544 * 1545 * @return string 1546 */ 1547 public function youngestMarriageFemaleAge(string $show_years = ''): string 1548 { 1549 return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); 1550 } 1551 1552 /** 1553 * Find the oldest wife. 1554 * 1555 * @return string 1556 */ 1557 public function oldestMarriageFemale(): string 1558 { 1559 return $this->marriageQuery('full', 'DESC', 'F', false); 1560 } 1561 1562 /** 1563 * Find the name of the oldest wife. 1564 * 1565 * @return string 1566 */ 1567 public function oldestMarriageFemaleName(): string 1568 { 1569 return $this->marriageQuery('name', 'DESC', 'F', false); 1570 } 1571 1572 /** 1573 * Find the age of the oldest wife. 1574 * 1575 * @param string $show_years 1576 * 1577 * @return string 1578 */ 1579 public function oldestMarriageFemaleAge(string $show_years = ''): string 1580 { 1581 return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); 1582 } 1583 1584 /** 1585 * Find the youngest husband. 1586 * 1587 * @return string 1588 */ 1589 public function youngestMarriageMale(): string 1590 { 1591 return $this->marriageQuery('full', 'ASC', 'M', false); 1592 } 1593 1594 /** 1595 * Find the name of the youngest husband. 1596 * 1597 * @return string 1598 */ 1599 public function youngestMarriageMaleName(): string 1600 { 1601 return $this->marriageQuery('name', 'ASC', 'M', false); 1602 } 1603 1604 /** 1605 * Find the age of the youngest husband. 1606 * 1607 * @param string $show_years 1608 * 1609 * @return string 1610 */ 1611 public function youngestMarriageMaleAge(string $show_years = ''): string 1612 { 1613 return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); 1614 } 1615 1616 /** 1617 * Find the oldest husband. 1618 * 1619 * @return string 1620 */ 1621 public function oldestMarriageMale(): string 1622 { 1623 return $this->marriageQuery('full', 'DESC', 'M', false); 1624 } 1625 1626 /** 1627 * Find the name of the oldest husband. 1628 * 1629 * @return string 1630 */ 1631 public function oldestMarriageMaleName(): string 1632 { 1633 return $this->marriageQuery('name', 'DESC', 'M', false); 1634 } 1635 1636 /** 1637 * Find the age of the oldest husband. 1638 * 1639 * @param string $show_years 1640 * 1641 * @return string 1642 */ 1643 public function oldestMarriageMaleAge(string $show_years = ''): string 1644 { 1645 return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); 1646 } 1647 1648 /** 1649 * General query on marriages. 1650 * 1651 * @param int $year1 1652 * @param int $year2 1653 * 1654 * @return Builder 1655 */ 1656 public function statsMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1657 { 1658 $query = DB::table('dates') 1659 ->where('d_file', '=', $this->tree->id()) 1660 ->where('d_fact', '=', 'MARR') 1661 ->select(['d_month', new Expression('COUNT(*) AS total')]) 1662 ->groupBy(['d_month']); 1663 1664 if ($year1 >= 0 && $year2 >= 0) { 1665 $query->whereBetween('d_year', [$year1, $year2]); 1666 } 1667 1668 return $query; 1669 } 1670 1671 /** 1672 * General query on marriages. 1673 * 1674 * @param int $year1 1675 * @param int $year2 1676 * 1677 * @return Builder 1678 */ 1679 public function statsFirstMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1680 { 1681 $query = DB::table('families') 1682 ->join('dates', static function (JoinClause $join): void { 1683 $join 1684 ->on('d_gid', '=', 'f_id') 1685 ->on('d_file', '=', 'f_file') 1686 ->where('d_fact', '=', 'MARR') 1687 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']) 1688 ->where('d_julianday2', '<>', 0); 1689 })->join('individuals', static function (JoinClause $join): void { 1690 $join 1691 ->on('i_file', '=', 'f_file'); 1692 }) 1693 ->where('f_file', '=', $this->tree->id()) 1694 ->where(static function (Builder $query): void { 1695 $query 1696 ->whereColumn('i_id', '=', 'f_husb') 1697 ->orWhereColumn('i_id', '=', 'f_wife'); 1698 }); 1699 1700 if ($year1 >= 0 && $year2 >= 0) { 1701 $query->whereBetween('d_year', [$year1, $year2]); 1702 } 1703 1704 return $query 1705 ->select(['f_id AS fams', 'f_husb', 'f_wife', 'd_julianday2 AS age', 'd_month AS month', 'i_id AS indi']) 1706 ->orderBy('f_id') 1707 ->orderBy('i_id') 1708 ->orderBy('d_julianday2'); 1709 } 1710 1711 /** 1712 * General query on marriages. 1713 * 1714 * @param string|null $color_from 1715 * @param string|null $color_to 1716 * 1717 * @return string 1718 */ 1719 public function statsMarr(string $color_from = null, string $color_to = null): string 1720 { 1721 return (new ChartMarriage($this->tree)) 1722 ->chartMarriage($color_from, $color_to); 1723 } 1724 1725 /** 1726 * General divorce query. 1727 * 1728 * @param string|null $color_from 1729 * @param string|null $color_to 1730 * 1731 * @return string 1732 */ 1733 public function statsDiv(string $color_from = null, string $color_to = null): string 1734 { 1735 return (new ChartDivorce($this->tree)) 1736 ->chartDivorce($color_from, $color_to); 1737 } 1738} 1739