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