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