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