1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Date; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\Family; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Individual; 28use Fisharebest\Webtrees\Services\ClipboardService; 29use Fisharebest\Webtrees\Services\ModuleService; 30use Illuminate\Support\Collection; 31 32use function str_contains; 33 34/** 35 * Class IndividualFactsTabModule 36 */ 37class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterface 38{ 39 use ModuleTabTrait; 40 41 private ModuleService $module_service; 42 43 private ClipboardService $clipboard_service; 44 45 /** 46 * IndividualFactsTabModule constructor. 47 * 48 * @param ModuleService $module_service 49 * @param ClipboardService $clipboard_service 50 */ 51 public function __construct(ModuleService $module_service, ClipboardService $clipboard_service) 52 { 53 $this->module_service = $module_service; 54 $this->clipboard_service = $clipboard_service; 55 } 56 57 /** 58 * How should this module be identified in the control panel, etc.? 59 * 60 * @return string 61 */ 62 public function title(): string 63 { 64 /* I18N: Name of a module/tab on the individual page. */ 65 return I18N::translate('Facts and events'); 66 } 67 68 /** 69 * A sentence describing what this module does. 70 * 71 * @return string 72 */ 73 public function description(): string 74 { 75 /* I18N: Description of the “Facts and events” module */ 76 return I18N::translate('A tab showing the facts and events of an individual.'); 77 } 78 79 /** 80 * The default position for this tab. It can be changed in the control panel. 81 * 82 * @return int 83 */ 84 public function defaultTabOrder(): int 85 { 86 return 1; 87 } 88 89 /** 90 * A greyed out tab has no actual content, but may perhaps have 91 * options to create content. 92 * 93 * @param Individual $individual 94 * 95 * @return bool 96 */ 97 public function isGrayedOut(Individual $individual): bool 98 { 99 return false; 100 } 101 102 /** 103 * Generate the HTML content of this tab. 104 * 105 * @param Individual $individual 106 * 107 * @return string 108 */ 109 public function getTabContent(Individual $individual): string 110 { 111 // Only include events of close relatives that are between birth and death 112 $min_date = $individual->getEstimatedBirthDate(); 113 $max_date = $individual->getEstimatedDeathDate(); 114 115 // Which facts and events are handled by other modules? 116 $sidebar_facts = $this->module_service 117 ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user()) 118 ->map(static function (ModuleSidebarInterface $sidebar): Collection { 119 return $sidebar->supportedFacts(); 120 }); 121 122 $tab_facts = $this->module_service 123 ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user()) 124 ->map(static function (ModuleTabInterface $sidebar): Collection { 125 return $sidebar->supportedFacts(); 126 }); 127 128 $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten(); 129 130 // The individual’s own facts 131 $individual_facts = $individual->facts() 132 ->filter(static function (Fact $fact) use ($exclude_facts): bool { 133 return !$exclude_facts->contains($fact->getTag()); 134 }); 135 136 $relative_facts = new Collection(); 137 138 // Add spouse-family facts 139 foreach ($individual->spouseFamilies() as $family) { 140 foreach ($family->facts() as $fact) { 141 if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') { 142 $relative_facts->push($fact); 143 } 144 } 145 146 $spouse = $family->spouse($individual); 147 148 if ($spouse instanceof Individual) { 149 $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date); 150 $relative_facts = $relative_facts->merge($spouse_facts); 151 } 152 153 $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date); 154 $relative_facts = $relative_facts->merge($child_facts); 155 } 156 157 $parent_facts = $this->parentFacts($individual, 1, $min_date, $max_date); 158 $relative_facts = $relative_facts->merge($parent_facts); 159 $associate_facts = $this->associateFacts($individual); 160 $historic_facts = $this->historicFacts($individual); 161 162 $individual_facts = $individual_facts 163 ->merge($associate_facts) 164 ->merge($historic_facts) 165 ->merge($relative_facts); 166 167 $individual_facts = Fact::sortFacts($individual_facts); 168 169 return view('modules/personal_facts/tab', [ 170 'can_edit' => $individual->canEdit(), 171 'clipboard_facts' => $this->clipboard_service->pastableFacts($individual), 172 'has_associate_facts' => $associate_facts->isNotEmpty(), 173 'has_historic_facts' => $historic_facts->isNotEmpty(), 174 'has_relative_facts' => $relative_facts->isNotEmpty(), 175 'individual' => $individual, 176 'facts' => $individual_facts, 177 ]); 178 } 179 180 /** 181 * Spouse facts that are shown on an individual’s page. 182 * 183 * @param Individual $individual Show events that occured during the lifetime of this individual 184 * @param Individual $spouse Show events of this individual 185 * @param Date $min_date 186 * @param Date $max_date 187 * 188 * @return Collection<Fact> 189 */ 190 private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): Collection 191 { 192 $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 193 194 $death_of_a_spouse = [ 195 'DEAT' => [ 196 'M' => I18N::translate('Death of a husband'), 197 'F' => I18N::translate('Death of a wife'), 198 'U' => I18N::translate('Death of a spouse'), 199 ], 200 'BURI' => [ 201 'M' => I18N::translate('Burial of a husband'), 202 'F' => I18N::translate('Burial of a wife'), 203 'U' => I18N::translate('Burial of a spouse'), 204 ], 205 'CREM' => [ 206 'M' => I18N::translate('Cremation of a husband'), 207 'F' => I18N::translate('Cremation of a wife'), 208 'U' => I18N::translate('Cremation of a spouse'), 209 ], 210 ]; 211 212 $facts = new Collection(); 213 214 if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) { 215 foreach ($spouse->facts(['DEAT', 'BURI', 'CREM']) as $fact) { 216 if ($this->includeFact($fact, $min_date, $max_date)) { 217 $facts[] = $this->convertEvent($fact, $death_of_a_spouse[$fact->getTag()][$fact->record()->sex()]); 218 } 219 } 220 } 221 222 return $facts; 223 } 224 225 /** 226 * Does a relative event occur within a date range (i.e. the individual's lifetime)? 227 * 228 * @param Fact $fact 229 * @param Date $min_date 230 * @param Date $max_date 231 * 232 * @return bool 233 */ 234 private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool 235 { 236 $fact_date = $fact->date(); 237 238 return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0; 239 } 240 241 /** 242 * Convert an event into a special "event of a close relative". 243 * 244 * @param Fact $fact 245 * @param string $type 246 * 247 * @return Fact 248 */ 249 private function convertEvent(Fact $fact, string $type): Fact 250 { 251 $gedcom = $fact->gedcom(); 252 $gedcom = preg_replace('/\n2 TYPE .*/', '', $gedcom); 253 $gedcom = preg_replace('/^1 .*/', "1 EVEN CLOSE_RELATIVE\n2 TYPE " . $type, $gedcom); 254 255 $converted = new Fact($gedcom, $fact->record(), $fact->id()); 256 257 if ($fact->isPendingAddition()) { 258 $converted->setPendingAddition(); 259 } 260 261 if ($fact->isPendingDeletion()) { 262 $converted->setPendingDeletion(); 263 } 264 265 return $converted; 266 } 267 268 /** 269 * Get the events of children and grandchildren. 270 * 271 * @param Individual $person 272 * @param Family $family 273 * @param string $option 274 * @param string $relation 275 * @param Date $min_date 276 * @param Date $max_date 277 * 278 * @return Collection<Fact> 279 */ 280 private function childFacts(Individual $person, Family $family, string $option, string $relation, Date $min_date, Date $max_date): Collection 281 { 282 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 283 284 $birth_of_a_child = [ 285 'BIRT' => [ 286 'M' => I18N::translate('Birth of a son'), 287 'F' => I18N::translate('Birth of a daughter'), 288 'U' => I18N::translate('Birth of a child'), 289 ], 290 'CHR' => [ 291 'M' => I18N::translate('Christening of a son'), 292 'F' => I18N::translate('Christening of a daughter'), 293 'U' => I18N::translate('Christening of a child'), 294 ], 295 'BAPM' => [ 296 'M' => I18N::translate('Baptism of a son'), 297 'F' => I18N::translate('Baptism of a daughter'), 298 'U' => I18N::translate('Baptism of a child'), 299 ], 300 'ADOP' => [ 301 'M' => I18N::translate('Adoption of a son'), 302 'F' => I18N::translate('Adoption of a daughter'), 303 'U' => I18N::translate('Adoption of a child'), 304 ], 305 ]; 306 307 $birth_of_a_sibling = [ 308 'BIRT' => [ 309 'M' => I18N::translate('Birth of a brother'), 310 'F' => I18N::translate('Birth of a sister'), 311 'U' => I18N::translate('Birth of a sibling'), 312 ], 313 'CHR' => [ 314 'M' => I18N::translate('Christening of a brother'), 315 'F' => I18N::translate('Christening of a sister'), 316 'U' => I18N::translate('Christening of a sibling'), 317 ], 318 'BAPM' => [ 319 'M' => I18N::translate('Baptism of a brother'), 320 'F' => I18N::translate('Baptism of a sister'), 321 'U' => I18N::translate('Baptism of a sibling'), 322 ], 323 'ADOP' => [ 324 'M' => I18N::translate('Adoption of a brother'), 325 'F' => I18N::translate('Adoption of a sister'), 326 'U' => I18N::translate('Adoption of a sibling'), 327 ], 328 ]; 329 330 $birth_of_a_half_sibling = [ 331 'BIRT' => [ 332 'M' => I18N::translate('Birth of a half-brother'), 333 'F' => I18N::translate('Birth of a half-sister'), 334 'U' => I18N::translate('Birth of a half-sibling'), 335 ], 336 'CHR' => [ 337 'M' => I18N::translate('Christening of a half-brother'), 338 'F' => I18N::translate('Christening of a half-sister'), 339 'U' => I18N::translate('Christening of a half-sibling'), 340 ], 341 'BAPM' => [ 342 'M' => I18N::translate('Baptism of a half-brother'), 343 'F' => I18N::translate('Baptism of a half-sister'), 344 'U' => I18N::translate('Baptism of a half-sibling'), 345 ], 346 'ADOP' => [ 347 'M' => I18N::translate('Adoption of a half-brother'), 348 'F' => I18N::translate('Adoption of a half-sister'), 349 'U' => I18N::translate('Adoption of a half-sibling'), 350 ], 351 ]; 352 353 $birth_of_a_grandchild = [ 354 'BIRT' => [ 355 'M' => I18N::translate('Birth of a grandson'), 356 'F' => I18N::translate('Birth of a granddaughter'), 357 'U' => I18N::translate('Birth of a grandchild'), 358 ], 359 'CHR' => [ 360 'M' => I18N::translate('Christening of a grandson'), 361 'F' => I18N::translate('Christening of a granddaughter'), 362 'U' => I18N::translate('Christening of a grandchild'), 363 ], 364 'BAPM' => [ 365 'M' => I18N::translate('Baptism of a grandson'), 366 'F' => I18N::translate('Baptism of a granddaughter'), 367 'U' => I18N::translate('Baptism of a grandchild'), 368 ], 369 'ADOP' => [ 370 'M' => I18N::translate('Adoption of a grandson'), 371 'F' => I18N::translate('Adoption of a granddaughter'), 372 'U' => I18N::translate('Adoption of a grandchild'), 373 ], 374 ]; 375 376 $birth_of_a_grandchild1 = [ 377 'BIRT' => [ 378 'M' => I18N::translateContext('daughter’s son', 'Birth of a grandson'), 379 'F' => I18N::translateContext('daughter’s daughter', 'Birth of a granddaughter'), 380 'U' => I18N::translate('Birth of a grandchild'), 381 ], 382 'CHR' => [ 383 'M' => I18N::translateContext('daughter’s son', 'Christening of a grandson'), 384 'F' => I18N::translateContext('daughter’s daughter', 'Christening of a granddaughter'), 385 'U' => I18N::translate('Christening of a grandchild'), 386 ], 387 'BAPM' => [ 388 'M' => I18N::translateContext('daughter’s son', 'Baptism of a grandson'), 389 'F' => I18N::translateContext('daughter’s daughter', 'Baptism of a granddaughter'), 390 'U' => I18N::translate('Baptism of a grandchild'), 391 ], 392 'ADOP' => [ 393 'M' => I18N::translateContext('daughter’s son', 'Adoption of a grandson'), 394 'F' => I18N::translateContext('daughter’s daughter', 'Adoption of a granddaughter'), 395 'U' => I18N::translate('Adoption of a grandchild'), 396 ], 397 ]; 398 399 $birth_of_a_grandchild2 = [ 400 'BIRT' => [ 401 'M' => I18N::translateContext('son’s son', 'Birth of a grandson'), 402 'F' => I18N::translateContext('son’s daughter', 'Birth of a granddaughter'), 403 'U' => I18N::translate('Birth of a grandchild'), 404 ], 405 'CHR' => [ 406 'M' => I18N::translateContext('son’s son', 'Christening of a grandson'), 407 'F' => I18N::translateContext('son’s daughter', 'Christening of a granddaughter'), 408 'U' => I18N::translate('Christening of a grandchild'), 409 ], 410 'BAPM' => [ 411 'M' => I18N::translateContext('son’s son', 'Baptism of a grandson'), 412 'F' => I18N::translateContext('son’s daughter', 'Baptism of a granddaughter'), 413 'U' => I18N::translate('Baptism of a grandchild'), 414 ], 415 'ADOP' => [ 416 'M' => I18N::translateContext('son’s son', 'Adoption of a grandson'), 417 'F' => I18N::translateContext('son’s daughter', 'Adoption of a granddaughter'), 418 'U' => I18N::translate('Adoption of a grandchild'), 419 ], 420 ]; 421 422 $death_of_a_child = [ 423 'DEAT' => [ 424 'M' => I18N::translate('Death of a son'), 425 'F' => I18N::translate('Death of a daughter'), 426 'U' => I18N::translate('Death of a child'), 427 ], 428 'BURI' => [ 429 'M' => I18N::translate('Burial of a son'), 430 'F' => I18N::translate('Burial of a daughter'), 431 'U' => I18N::translate('Burial of a child'), 432 ], 433 'CREM' => [ 434 'M' => I18N::translate('Cremation of a son'), 435 'F' => I18N::translate('Cremation of a daughter'), 436 'U' => I18N::translate('Cremation of a child'), 437 ], 438 ]; 439 440 $death_of_a_sibling = [ 441 'DEAT' => [ 442 'M' => I18N::translate('Death of a brother'), 443 'F' => I18N::translate('Death of a sister'), 444 'U' => I18N::translate('Death of a sibling'), 445 ], 446 'BURI' => [ 447 'M' => I18N::translate('Burial of a brother'), 448 'F' => I18N::translate('Burial of a sister'), 449 'U' => I18N::translate('Burial of a sibling'), 450 ], 451 'CREM' => [ 452 'M' => I18N::translate('Cremation of a brother'), 453 'F' => I18N::translate('Cremation of a sister'), 454 'U' => I18N::translate('Cremation of a sibling'), 455 ], 456 ]; 457 458 $death_of_a_half_sibling = [ 459 'DEAT' => [ 460 'M' => I18N::translate('Death of a half-brother'), 461 'F' => I18N::translate('Death of a half-sister'), 462 'U' => I18N::translate('Death of a half-sibling'), 463 ], 464 'BURI' => [ 465 'M' => I18N::translate('Burial of a half-brother'), 466 'F' => I18N::translate('Burial of a half-sister'), 467 'U' => I18N::translate('Burial of a half-sibling'), 468 ], 469 'CREM' => [ 470 'M' => I18N::translate('Cremation of a half-brother'), 471 'F' => I18N::translate('Cremation of a half-sister'), 472 'U' => I18N::translate('Cremation of a half-sibling'), 473 ], 474 ]; 475 476 $death_of_a_grandchild = [ 477 'DEAT' => [ 478 'M' => I18N::translate('Death of a grandson'), 479 'F' => I18N::translate('Death of a granddaughter'), 480 'U' => I18N::translate('Death of a grandchild'), 481 ], 482 'BURI' => [ 483 'M' => I18N::translate('Burial of a grandson'), 484 'F' => I18N::translate('Burial of a granddaughter'), 485 'U' => I18N::translate('Burial of a grandchild'), 486 ], 487 'CREM' => [ 488 'M' => I18N::translate('Cremation of a grandson'), 489 'F' => I18N::translate('Cremation of a granddaughter'), 490 'U' => I18N::translate('Baptism of a grandchild'), 491 ], 492 ]; 493 494 $death_of_a_grandchild1 = [ 495 'DEAT' => [ 496 'M' => I18N::translateContext('daughter’s son', 'Death of a grandson'), 497 'F' => I18N::translateContext('daughter’s daughter', 'Death of a granddaughter'), 498 'U' => I18N::translate('Death of a grandchild'), 499 ], 500 'BURI' => [ 501 'M' => I18N::translateContext('daughter’s son', 'Burial of a grandson'), 502 'F' => I18N::translateContext('daughter’s daughter', 'Burial of a granddaughter'), 503 'U' => I18N::translate('Burial of a grandchild'), 504 ], 505 'CREM' => [ 506 'M' => I18N::translateContext('daughter’s son', 'Cremation of a grandson'), 507 'F' => I18N::translateContext('daughter’s daughter', 'Cremation of a granddaughter'), 508 'U' => I18N::translate('Baptism of a grandchild'), 509 ], 510 ]; 511 512 $death_of_a_grandchild2 = [ 513 'DEAT' => [ 514 'M' => I18N::translateContext('son’s son', 'Death of a grandson'), 515 'F' => I18N::translateContext('son’s daughter', 'Death of a granddaughter'), 516 'U' => I18N::translate('Death of a grandchild'), 517 ], 518 'BURI' => [ 519 'M' => I18N::translateContext('son’s son', 'Burial of a grandson'), 520 'F' => I18N::translateContext('son’s daughter', 'Burial of a granddaughter'), 521 'U' => I18N::translate('Burial of a grandchild'), 522 ], 523 'CREM' => [ 524 'M' => I18N::translateContext('son’s son', 'Cremation of a grandson'), 525 'F' => I18N::translateContext('son’s daughter', 'Cremation of a granddaughter'), 526 'U' => I18N::translate('Cremation of a grandchild'), 527 ], 528 ]; 529 530 $marriage_of_a_child = [ 531 'M' => I18N::translate('Marriage of a son'), 532 'F' => I18N::translate('Marriage of a daughter'), 533 'U' => I18N::translate('Marriage of a child'), 534 ]; 535 536 $marriage_of_a_grandchild = [ 537 'M' => I18N::translate('Marriage of a grandson'), 538 'F' => I18N::translate('Marriage of a granddaughter'), 539 'U' => I18N::translate('Marriage of a grandchild'), 540 ]; 541 542 $marriage_of_a_grandchild1 = [ 543 'M' => I18N::translateContext('daughter’s son', 'Marriage of a grandson'), 544 'F' => I18N::translateContext('daughter’s daughter', 'Marriage of a granddaughter'), 545 'U' => I18N::translate('Marriage of a grandchild'), 546 ]; 547 548 $marriage_of_a_grandchild2 = [ 549 'M' => I18N::translateContext('son’s son', 'Marriage of a grandson'), 550 'F' => I18N::translateContext('son’s daughter', 'Marriage of a granddaughter'), 551 'U' => I18N::translate('Marriage of a grandchild'), 552 ]; 553 554 $marriage_of_a_sibling = [ 555 'M' => I18N::translate('Marriage of a brother'), 556 'F' => I18N::translate('Marriage of a sister'), 557 'U' => I18N::translate('Marriage of a sibling'), 558 ]; 559 560 $marriage_of_a_half_sibling = [ 561 'M' => I18N::translate('Marriage of a half-brother'), 562 'F' => I18N::translate('Marriage of a half-sister'), 563 'U' => I18N::translate('Marriage of a half-sibling'), 564 ]; 565 566 $facts = new Collection(); 567 568 // Deal with recursion. 569 switch ($option) { 570 case '_CHIL': 571 // Add grandchildren 572 foreach ($family->children() as $child) { 573 foreach ($child->spouseFamilies() as $cfamily) { 574 switch ($child->sex()) { 575 case 'M': 576 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) { 577 $facts[] = $fact; 578 } 579 break; 580 case 'F': 581 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) { 582 $facts[] = $fact; 583 } 584 break; 585 default: 586 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) { 587 $facts[] = $fact; 588 } 589 break; 590 } 591 } 592 } 593 break; 594 } 595 596 // For each child in the family 597 foreach ($family->children() as $child) { 598 if ($child->xref() === $person->xref()) { 599 // We are not our own sibling! 600 continue; 601 } 602 // add child’s birth 603 if (str_contains($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option))) { 604 foreach ($child->facts(['BIRT', 'CHR', 'BAPM', 'ADOP']) as $fact) { 605 // Always show _BIRT_CHIL, even if the dates are not known 606 if ($option === '_CHIL' || $this->includeFact($fact, $min_date, $max_date)) { 607 switch ($option) { 608 case '_GCHI': 609 switch ($relation) { 610 case 'dau': 611 $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild1[$fact->getTag()][$fact->record()->sex()]); 612 break; 613 case 'son': 614 $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild2[$fact->getTag()][$fact->record()->sex()]); 615 break; 616 case 'chil': 617 $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild[$fact->getTag()][$fact->record()->sex()]); 618 break; 619 } 620 break; 621 case '_SIBL': 622 $facts[] = $this->convertEvent($fact, $birth_of_a_sibling[$fact->getTag()][$fact->record()->sex()]); 623 break; 624 case '_HSIB': 625 $facts[] = $this->convertEvent($fact, $birth_of_a_half_sibling[$fact->getTag()][$fact->record()->sex()]); 626 break; 627 case '_CHIL': 628 $facts[] = $this->convertEvent($fact, $birth_of_a_child[$fact->getTag()][$fact->record()->sex()]); 629 break; 630 } 631 } 632 } 633 } 634 // add child’s death 635 if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option))) { 636 foreach ($child->facts(['DEAT', 'BURI', 'CREM']) as $fact) { 637 if ($this->includeFact($fact, $min_date, $max_date)) { 638 switch ($option) { 639 case '_GCHI': 640 switch ($relation) { 641 case 'dau': 642 $facts[] = $this->convertEvent($fact, $death_of_a_grandchild1[$fact->getTag()][$fact->record()->sex()]); 643 break; 644 case 'son': 645 $facts[] = $this->convertEvent($fact, $death_of_a_grandchild2[$fact->getTag()][$fact->record()->sex()]); 646 break; 647 case 'chi': 648 $facts[] = $this->convertEvent($fact, $death_of_a_grandchild[$fact->getTag()][$fact->record()->sex()]); 649 break; 650 } 651 break; 652 case '_SIBL': 653 $facts[] = $this->convertEvent($fact, $death_of_a_sibling[$fact->getTag()][$fact->record()->sex()]); 654 break; 655 case '_HSIB': 656 $facts[] = $this->convertEvent($fact, $death_of_a_half_sibling[$fact->getTag()][$fact->record()->sex()]); 657 break; 658 case '_CHIL': 659 $facts[] = $this->convertEvent($fact, $death_of_a_child[$fact->getTag()][$fact->record()->sex()]); 660 break; 661 } 662 } 663 } 664 } 665 666 // add child’s marriage 667 if (str_contains($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) { 668 foreach ($child->spouseFamilies() as $sfamily) { 669 foreach ($sfamily->facts(['MARR']) as $fact) { 670 if ($this->includeFact($fact, $min_date, $max_date)) { 671 switch ($option) { 672 case '_GCHI': 673 switch ($relation) { 674 case 'dau': 675 $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild1['F']); 676 break; 677 case 'son': 678 $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild2['M']); 679 break; 680 case 'chi': 681 $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild['U']); 682 break; 683 } 684 break; 685 case '_SIBL': 686 $facts[] = $this->convertEvent($fact, $marriage_of_a_sibling['U']); 687 break; 688 case '_HSIB': 689 $facts[] = $this->convertEvent($fact, $marriage_of_a_half_sibling['U']); 690 break; 691 case '_CHIL': 692 $facts[] = $this->convertEvent($fact, $marriage_of_a_child['U']); 693 break; 694 } 695 } 696 } 697 } 698 } 699 } 700 701 return $facts; 702 } 703 704 /** 705 * Get the events of parents and grandparents. 706 * 707 * @param Individual $person 708 * @param int $sosa 709 * @param Date $min_date 710 * @param Date $max_date 711 * 712 * @return Collection<Fact> 713 */ 714 private function parentFacts(Individual $person, int $sosa, Date $min_date, Date $max_date): Collection 715 { 716 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 717 718 $death_of_a_parent = [ 719 'DEAT' => [ 720 'M' => I18N::translate('Death of a father'), 721 'F' => I18N::translate('Death of a mother'), 722 'U' => I18N::translate('Death of a parent'), 723 ], 724 'BURI' => [ 725 'M' => I18N::translate('Burial of a father'), 726 'F' => I18N::translate('Burial of a mother'), 727 'U' => I18N::translate('Burial of a parent'), 728 ], 729 'CREM' => [ 730 'M' => I18N::translate('Cremation of a father'), 731 'F' => I18N::translate('Cremation of a mother'), 732 'U' => I18N::translate('Cremation of a parent'), 733 ], 734 ]; 735 736 $death_of_a_grandparent = [ 737 'DEAT' => [ 738 'M' => I18N::translate('Death of a grandfather'), 739 'F' => I18N::translate('Death of a grandmother'), 740 'U' => I18N::translate('Death of a grandparent'), 741 ], 742 'BURI' => [ 743 'M' => I18N::translate('Burial of a grandfather'), 744 'F' => I18N::translate('Burial of a grandmother'), 745 'U' => I18N::translate('Burial of a grandparent'), 746 ], 747 'CREM' => [ 748 'M' => I18N::translate('Cremation of a grandfather'), 749 'F' => I18N::translate('Cremation of a grandmother'), 750 'U' => I18N::translate('Cremation of a grandparent'), 751 ], 752 ]; 753 754 $death_of_a_maternal_grandparent = [ 755 'DEAT' => [ 756 'M' => I18N::translate('Death of a maternal grandfather'), 757 'F' => I18N::translate('Death of a maternal grandmother'), 758 'U' => I18N::translate('Death of a grandparent'), 759 ], 760 'BURI' => [ 761 'M' => I18N::translate('Burial of a maternal grandfather'), 762 'F' => I18N::translate('Burial of a maternal grandmother'), 763 'U' => I18N::translate('Burial of a grandparent'), 764 ], 765 'CREM' => [ 766 'M' => I18N::translate('Cremation of a maternal grandfather'), 767 'F' => I18N::translate('Cremation of a maternal grandmother'), 768 'U' => I18N::translate('Cremation of a grandparent'), 769 ], 770 ]; 771 772 $death_of_a_paternal_grandparent = [ 773 'DEAT' => [ 774 'M' => I18N::translate('Death of a paternal grandfather'), 775 'F' => I18N::translate('Death of a paternal grandmother'), 776 'U' => I18N::translate('Death of a grandparent'), 777 ], 778 'BURI' => [ 779 'M' => I18N::translate('Burial of a paternal grandfather'), 780 'F' => I18N::translate('Burial of a paternal grandmother'), 781 'U' => I18N::translate('Burial of a grandparent'), 782 ], 783 'CREM' => [ 784 'M' => I18N::translate('Cremation of a paternal grandfather'), 785 'F' => I18N::translate('Cremation of a paternal grandmother'), 786 'U' => I18N::translate('Cremation of a grandparent'), 787 ], 788 ]; 789 790 $marriage_of_a_parent = [ 791 'M' => I18N::translate('Marriage of a father'), 792 'F' => I18N::translate('Marriage of a mother'), 793 'U' => I18N::translate('Marriage of a parent'), 794 ]; 795 796 $facts = new Collection(); 797 798 if ($sosa === 1) { 799 foreach ($person->childFamilies() as $family) { 800 // Add siblings 801 foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) { 802 $facts[] = $fact; 803 } 804 foreach ($family->spouses() as $spouse) { 805 foreach ($spouse->spouseFamilies() as $sfamily) { 806 if ($family !== $sfamily) { 807 // Add half-siblings 808 foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) { 809 $facts[] = $fact; 810 } 811 } 812 } 813 // Add grandparents 814 foreach ($this->parentFacts($spouse, $spouse->sex() === 'F' ? 3 : 2, $min_date, $max_date) as $fact) { 815 $facts[] = $fact; 816 } 817 } 818 } 819 820 if (str_contains($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) { 821 // add father/mother marriages 822 foreach ($person->childFamilies() as $sfamily) { 823 foreach ($sfamily->facts(['MARR']) as $fact) { 824 if ($this->includeFact($fact, $min_date, $max_date)) { 825 // marriage of parents (to each other) 826 $facts[] = $this->convertEvent($fact, I18N::translate('Marriage of parents')); 827 } 828 } 829 } 830 foreach ($person->childStepFamilies() as $sfamily) { 831 foreach ($sfamily->facts(['MARR']) as $fact) { 832 if ($this->includeFact($fact, $min_date, $max_date)) { 833 // marriage of a parent (to another spouse) 834 $facts[] = $this->convertEvent($fact, $marriage_of_a_parent['U']); 835 } 836 } 837 } 838 } 839 } 840 841 foreach ($person->childFamilies() as $family) { 842 foreach ($family->spouses() as $parent) { 843 if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa === 1 ? '_PARE' : '_GPAR'))) { 844 foreach ($parent->facts(['DEAT', 'BURI', 'CREM']) as $fact) { 845 // Show death of parent when it happened prior to birth 846 if ($sosa === 1 && Date::compare($fact->date(), $min_date) < 0 || $this->includeFact($fact, $min_date, $max_date)) { 847 switch ($sosa) { 848 case 1: 849 $facts[] = $this->convertEvent($fact, $death_of_a_parent[$fact->getTag()][$fact->record()->sex()]); 850 break; 851 case 2: 852 case 3: 853 switch ($person->sex()) { 854 case 'M': 855 $facts[] = $this->convertEvent($fact, $death_of_a_paternal_grandparent[$fact->getTag()][$fact->record()->sex()]); 856 break; 857 case 'F': 858 $facts[] = $this->convertEvent($fact, $death_of_a_maternal_grandparent[$fact->getTag()][$fact->record()->sex()]); 859 break; 860 default: 861 $facts[] = $this->convertEvent($fact, $death_of_a_grandparent[$fact->getTag()][$fact->record()->sex()]); 862 break; 863 } 864 } 865 } 866 } 867 } 868 } 869 } 870 871 return $facts; 872 } 873 874 /** 875 * Get the events of associates. 876 * 877 * @param Individual $person 878 * 879 * @return Collection<Fact> 880 */ 881 private function associateFacts(Individual $person): Collection 882 { 883 $facts = []; 884 885 /** @var Individual[] $associates */ 886 $asso1 = $person->linkedIndividuals('ASSO'); 887 $asso2 = $person->linkedIndividuals('_ASSO'); 888 $asso3 = $person->linkedFamilies('ASSO'); 889 $asso4 = $person->linkedFamilies('_ASSO'); 890 891 $associates = $asso1->merge($asso2)->merge($asso3)->merge($asso4); 892 893 foreach ($associates as $associate) { 894 foreach ($associate->facts() as $fact) { 895 if (preg_match('/\n\d _?ASSO @' . $person->xref() . '@/', $fact->gedcom())) { 896 // Extract the important details from the fact 897 $factrec = '1 ' . $fact->getTag(); 898 if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) { 899 $factrec .= $match[0]; 900 } 901 if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) { 902 $factrec .= $match[0]; 903 } 904 if ($associate instanceof Family) { 905 foreach ($associate->spouses() as $spouse) { 906 $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@'; 907 } 908 } else { 909 $factrec .= "\n2 _ASSO @" . $associate->xref() . '@'; 910 } 911 $facts[] = new Fact($factrec, $associate, 'asso'); 912 } 913 } 914 } 915 916 return new Collection($facts); 917 } 918 919 /** 920 * Get any historical events. 921 * 922 * @param Individual $individual 923 * 924 * @return Collection<Fact> 925 */ 926 private function historicFacts(Individual $individual): Collection 927 { 928 return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class) 929 ->map(static function (ModuleHistoricEventsInterface $module) use ($individual): Collection { 930 return $module->historicEventsForIndividual($individual); 931 }) 932 ->flatten(); 933 } 934 935 /** 936 * Is this tab empty? If so, we don't always need to display it. 937 * 938 * @param Individual $individual 939 * 940 * @return bool 941 */ 942 public function hasTabContent(Individual $individual): bool 943 { 944 return true; 945 } 946 947 /** 948 * Can this tab load asynchronously? 949 * 950 * @return bool 951 */ 952 public function canLoadAjax(): bool 953 { 954 return false; 955 } 956 957 /** 958 * This module handles the following facts - so don't show them on the "Facts and events" tab. 959 * 960 * @return Collection<string> 961 */ 962 public function supportedFacts(): Collection 963 { 964 // We don't actually displaye these facts, but they are displayed 965 // outside the tabs/sidebar systems. This just forces them to be excluded here. 966 return new Collection(['NAME', 'SEX', 'OBJE']); 967 } 968} 969