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