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