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