xref: /webtrees/app/Services/IndividualFactsService.php (revision 2ada9ea4cb12abae4cb4824ae5606c52b5b0be74)
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