xref: /webtrees/app/Module/IndividualFactsTabModule.php (revision 56d475424b46df195a76f2a149ee5a07327497e8)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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\Module;
21
22use Fisharebest\Webtrees\Auth;
23use Fisharebest\Webtrees\Date;
24use Fisharebest\Webtrees\Fact;
25use Fisharebest\Webtrees\Family;
26use Fisharebest\Webtrees\I18N;
27use Fisharebest\Webtrees\Individual;
28use Fisharebest\Webtrees\Services\ClipboardService;
29use Fisharebest\Webtrees\Services\ModuleService;
30use Illuminate\Support\Collection;
31
32use function str_contains;
33
34/**
35 * Class IndividualFactsTabModule
36 */
37class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterface
38{
39    use ModuleTabTrait;
40
41    /** @var ModuleService */
42    private $module_service;
43
44    /** @var ClipboardService */
45    private $clipboard_service;
46
47    /**
48     * IndividualFactsTabModule constructor.
49     *
50     * @param ModuleService    $module_service
51     * @param ClipboardService $clipboard_service
52     */
53    public function __construct(ModuleService $module_service, ClipboardService $clipboard_service)
54    {
55        $this->module_service    = $module_service;
56        $this->clipboard_service = $clipboard_service;
57    }
58
59    /**
60     * How should this module be identified in the control panel, etc.?
61     *
62     * @return string
63     */
64    public function title(): string
65    {
66        /* I18N: Name of a module/tab on the individual page. */
67        return I18N::translate('Facts and events');
68    }
69
70    /**
71     * A sentence describing what this module does.
72     *
73     * @return string
74     */
75    public function description(): string
76    {
77        /* I18N: Description of the “Facts and events” module */
78        return I18N::translate('A tab showing the facts and events of an individual.');
79    }
80
81    /**
82     * The default position for this tab.  It can be changed in the control panel.
83     *
84     * @return int
85     */
86    public function defaultTabOrder(): int
87    {
88        return 1;
89    }
90
91    /**
92     * A greyed out tab has no actual content, but may perhaps have
93     * options to create content.
94     *
95     * @param Individual $individual
96     *
97     * @return bool
98     */
99    public function isGrayedOut(Individual $individual): bool
100    {
101        return false;
102    }
103
104    /**
105     * Generate the HTML content of this tab.
106     *
107     * @param Individual $individual
108     *
109     * @return string
110     */
111    public function getTabContent(Individual $individual): string
112    {
113        // Only include events of close relatives that are between birth and death
114        $min_date = $individual->getEstimatedBirthDate();
115        $max_date = $individual->getEstimatedDeathDate();
116
117        // Which facts and events are handled by other modules?
118        $sidebar_facts = $this->module_service
119            ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
120            ->map(static function (ModuleSidebarInterface $sidebar): Collection {
121                return $sidebar->supportedFacts();
122            });
123
124        $tab_facts = $this->module_service
125            ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
126            ->map(static function (ModuleTabInterface $sidebar): Collection {
127                return $sidebar->supportedFacts();
128            });
129
130        $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten();
131
132        // The individual’s own facts
133        $indifacts = $individual->facts()
134            ->filter(static function (Fact $fact) use ($exclude_facts): bool {
135                return !$exclude_facts->contains($fact->getTag());
136            });
137
138        // Add spouse-family facts
139        foreach ($individual->spouseFamilies() as $family) {
140            foreach ($family->facts() as $fact) {
141                if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') {
142                    $indifacts->push($fact);
143                }
144            }
145
146            $spouse = $family->spouse($individual);
147
148            if ($spouse instanceof Individual) {
149                $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date);
150                $indifacts    = $indifacts->merge($spouse_facts);
151            }
152
153            $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date);
154            $indifacts   = $indifacts->merge($child_facts);
155        }
156
157        $parent_facts     = $this->parentFacts($individual, 1, $min_date, $max_date);
158        $associate_facts  = $this->associateFacts($individual);
159        $historical_facts = $this->historicalFacts($individual);
160
161        $indifacts = $indifacts
162            ->merge($parent_facts)
163            ->merge($associate_facts)
164            ->merge($historical_facts);
165
166        $indifacts = Fact::sortFacts($indifacts);
167
168        return view('modules/personal_facts/tab', [
169            'can_edit'             => $individual->canEdit(),
170            'clipboard_facts'      => $this->clipboard_service->pastableFacts($individual),
171            'has_historical_facts' => $historical_facts !== [],
172            'individual'           => $individual,
173            'facts'                => $indifacts,
174        ]);
175    }
176
177    /**
178     * Does a relative event occur within a date range (i.e. the individual's lifetime)?
179     *
180     * @param Fact $fact
181     * @param Date $min_date
182     * @param Date $max_date
183     *
184     * @return bool
185     */
186    private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool
187    {
188        $fact_date = $fact->date();
189
190        return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0;
191    }
192
193    /**
194     * Is this tab empty? If so, we don't always need to display it.
195     *
196     * @param Individual $individual
197     *
198     * @return bool
199     */
200    public function hasTabContent(Individual $individual): bool
201    {
202        return true;
203    }
204
205    /**
206     * Can this tab load asynchronously?
207     *
208     * @return bool
209     */
210    public function canLoadAjax(): bool
211    {
212        return false;
213    }
214
215    /**
216     * Convert an event into a special "event of a close relative".
217     *
218     * @param Fact   $fact
219     * @param string $type
220     *
221     * @return Fact
222     */
223    private function convertEvent(Fact $fact, string $type): Fact
224    {
225        $gedcom = $fact->gedcom();
226        $gedcom = preg_replace('/\n2 TYPE .*/', '', $gedcom);
227        $gedcom = preg_replace('/^1 .*/', "1 EVEN CLOSE_RELATIVE\n2 TYPE " . $type, $gedcom);
228
229        $converted = new Fact($gedcom, $fact->record(), $fact->id());
230
231        if ($fact->isPendingAddition()) {
232            $converted->setPendingAddition();
233        }
234
235        if ($fact->isPendingDeletion()) {
236            $converted->setPendingDeletion();
237        }
238
239        return $converted;
240    }
241
242    /**
243     * Spouse facts that are shown on an individual’s page.
244     *
245     * @param Individual $individual Show events that occured during the lifetime of this individual
246     * @param Individual $spouse     Show events of this individual
247     * @param Date       $min_date
248     * @param Date       $max_date
249     *
250     * @return Fact[]
251     */
252    private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): array
253    {
254        $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS');
255
256        $death_of_a_spouse = [
257            'DEAT' => [
258                'M' => I18N::translate('Death of a husband'),
259                'F' => I18N::translate('Death of a wife'),
260                'U' => I18N::translate('Death of a spouse'),
261            ],
262            'BURI' => [
263                'M' => I18N::translate('Burial of a husband'),
264                'F' => I18N::translate('Burial of a wife'),
265                'U' => I18N::translate('Burial of a spouse'),
266            ],
267            'CREM' => [
268                'M' => I18N::translate('Cremation of a husband'),
269                'F' => I18N::translate('Cremation of a wife'),
270                'U' => I18N::translate('Cremation of a spouse'),
271            ],
272        ];
273
274        $facts = [];
275
276        if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) {
277            foreach ($spouse->facts(['DEAT', 'BURI', 'CREM']) as $fact) {
278                if ($this->includeFact($fact, $min_date, $max_date)) {
279                    $facts[] = $this->convertEvent($fact, $death_of_a_spouse[$fact->getTag()][$fact->record()->sex()]);
280                }
281            }
282        }
283
284        return $facts;
285    }
286
287    /**
288     * Get the events of children and grandchildren.
289     *
290     * @param Individual $person
291     * @param Family     $family
292     * @param string     $option
293     * @param string     $relation
294     * @param Date       $min_date
295     * @param Date       $max_date
296     *
297     * @return Fact[]
298     */
299    private function childFacts(Individual $person, Family $family, string $option, string $relation, Date $min_date, Date $max_date): array
300    {
301        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
302
303        $birth_of_a_child = [
304            'BIRT' => [
305                'M' => I18N::translate('Birth of a son'),
306                'F' => I18N::translate('Birth of a daughter'),
307                'U' => I18N::translate('Birth of a child'),
308            ],
309            'CHR' => [
310                'M' => I18N::translate('Christening of a son'),
311                'F' => I18N::translate('Christening of a daughter'),
312                'U' => I18N::translate('Christening of a child'),
313            ],
314            'BAPM' => [
315                'M' => I18N::translate('Baptism of a son'),
316                'F' => I18N::translate('Baptism of a daughter'),
317                'U' => I18N::translate('Baptism of a child'),
318            ],
319            'ADOP' => [
320                'M' => I18N::translate('Adoption of a son'),
321                'F' => I18N::translate('Adoption of a daughter'),
322                'U' => I18N::translate('Adoption of a child'),
323            ],
324        ];
325
326        $birth_of_a_sibling = [
327            'BIRT' => [
328                'M' => I18N::translate('Birth of a brother'),
329                'F' => I18N::translate('Birth of a sister'),
330                'U' => I18N::translate('Birth of a sibling'),
331            ],
332            'CHR' => [
333                'M' => I18N::translate('Christening of a brother'),
334                'F' => I18N::translate('Christening of a sister'),
335                'U' => I18N::translate('Christening of a sibling'),
336            ],
337            'BAPM' => [
338                'M' => I18N::translate('Baptism of a brother'),
339                'F' => I18N::translate('Baptism of a sister'),
340                'U' => I18N::translate('Baptism of a sibling'),
341            ],
342            'ADOP' => [
343                'M' => I18N::translate('Adoption of a brother'),
344                'F' => I18N::translate('Adoption of a sister'),
345                'U' => I18N::translate('Adoption of a sibling'),
346            ],
347        ];
348
349        $birth_of_a_half_sibling = [
350            'BIRT' => [
351                'M' => I18N::translate('Birth of a half-brother'),
352                'F' => I18N::translate('Birth of a half-sister'),
353                'U' => I18N::translate('Birth of a half-sibling'),
354            ],
355            'CHR' => [
356                'M' => I18N::translate('Christening of a half-brother'),
357                'F' => I18N::translate('Christening of a half-sister'),
358                'U' => I18N::translate('Christening of a half-sibling'),
359            ],
360            'BAPM' => [
361                'M' => I18N::translate('Baptism of a half-brother'),
362                'F' => I18N::translate('Baptism of a half-sister'),
363                'U' => I18N::translate('Baptism of a half-sibling'),
364            ],
365            'ADOP' => [
366                'M' => I18N::translate('Adoption of a half-brother'),
367                'F' => I18N::translate('Adoption of a half-sister'),
368                'U' => I18N::translate('Adoption of a half-sibling'),
369            ],
370        ];
371
372        $birth_of_a_grandchild = [
373            'BIRT' => [
374                'M' => I18N::translate('Birth of a grandson'),
375                'F' => I18N::translate('Birth of a granddaughter'),
376                'U' => I18N::translate('Birth of a grandchild'),
377            ],
378            'CHR' => [
379                'M' => I18N::translate('Christening of a grandson'),
380                'F' => I18N::translate('Christening of a granddaughter'),
381                'U' => I18N::translate('Christening of a grandchild'),
382            ],
383            'BAPM' => [
384                'M' => I18N::translate('Baptism of a grandson'),
385                'F' => I18N::translate('Baptism of a granddaughter'),
386                'U' => I18N::translate('Baptism of a grandchild'),
387            ],
388            'ADOP' => [
389                'M' => I18N::translate('Adoption of a grandson'),
390                'F' => I18N::translate('Adoption of a granddaughter'),
391                'U' => I18N::translate('Adoption of a grandchild'),
392            ],
393        ];
394
395        $birth_of_a_grandchild1 = [
396            'BIRT' => [
397                'M' => I18N::translateContext('daughter’s son', 'Birth of a grandson'),
398                'F' => I18N::translateContext('daughter’s daughter', 'Birth of a granddaughter'),
399                'U' => I18N::translate('Birth of a grandchild'),
400            ],
401            'CHR' => [
402                'M' => I18N::translateContext('daughter’s son', 'Christening of a grandson'),
403                'F' => I18N::translateContext('daughter’s daughter', 'Christening of a granddaughter'),
404                'U' => I18N::translate('Christening of a grandchild'),
405            ],
406            'BAPM' => [
407                'M' => I18N::translateContext('daughter’s son', 'Baptism of a grandson'),
408                'F' => I18N::translateContext('daughter’s daughter', 'Baptism of a granddaughter'),
409                'U' => I18N::translate('Baptism of a grandchild'),
410            ],
411            'ADOP' => [
412                'M' => I18N::translateContext('daughter’s son', 'Adoption of a grandson'),
413                'F' => I18N::translateContext('daughter’s daughter', 'Adoption of a granddaughter'),
414                'U' => I18N::translate('Adoption of a grandchild'),
415            ],
416        ];
417
418        $birth_of_a_grandchild2 = [
419            'BIRT' => [
420                'M' => I18N::translateContext('son’s son', 'Birth of a grandson'),
421                'F' => I18N::translateContext('son’s daughter', 'Birth of a granddaughter'),
422                'U' => I18N::translate('Birth of a grandchild'),
423            ],
424            'CHR' => [
425                'M' => I18N::translateContext('son’s son', 'Christening of a grandson'),
426                'F' => I18N::translateContext('son’s daughter', 'Christening of a granddaughter'),
427                'U' => I18N::translate('Christening of a grandchild'),
428            ],
429            'BAPM' => [
430                'M' => I18N::translateContext('son’s son', 'Baptism of a grandson'),
431                'F' => I18N::translateContext('son’s daughter', 'Baptism of a granddaughter'),
432                'U' => I18N::translate('Baptism of a grandchild'),
433            ],
434            'ADOP' => [
435                'M' => I18N::translateContext('son’s son', 'Adoption of a grandson'),
436                'F' => I18N::translateContext('son’s daughter', 'Adoption of a granddaughter'),
437                'U' => I18N::translate('Adoption of a grandchild'),
438            ],
439        ];
440
441        $death_of_a_child = [
442            'DEAT' => [
443                'M' => I18N::translate('Death of a son'),
444                'F' => I18N::translate('Death of a daughter'),
445                'U' => I18N::translate('Death of a child'),
446            ],
447            'BURI' => [
448                'M' => I18N::translate('Burial of a son'),
449                'F' => I18N::translate('Burial of a daughter'),
450                'U' => I18N::translate('Burial of a child'),
451            ],
452            'CREM' => [
453                'M' => I18N::translate('Cremation of a son'),
454                'F' => I18N::translate('Cremation of a daughter'),
455                'U' => I18N::translate('Cremation of a child'),
456            ],
457        ];
458
459        $death_of_a_sibling = [
460            'DEAT' => [
461                'M' => I18N::translate('Death of a brother'),
462                'F' => I18N::translate('Death of a sister'),
463                'U' => I18N::translate('Death of a sibling'),
464            ],
465            'BURI' => [
466                'M' => I18N::translate('Burial of a brother'),
467                'F' => I18N::translate('Burial of a sister'),
468                'U' => I18N::translate('Burial of a sibling'),
469            ],
470            'CREM' => [
471                'M' => I18N::translate('Cremation of a brother'),
472                'F' => I18N::translate('Cremation of a sister'),
473                'U' => I18N::translate('Cremation of a sibling'),
474            ],
475        ];
476
477        $death_of_a_half_sibling = [
478            'DEAT' => [
479                'M' => I18N::translate('Death of a half-brother'),
480                'F' => I18N::translate('Death of a half-sister'),
481                'U' => I18N::translate('Death of a half-sibling'),
482            ],
483            'BURI' => [
484                'M' => I18N::translate('Burial of a half-brother'),
485                'F' => I18N::translate('Burial of a half-sister'),
486                'U' => I18N::translate('Burial of a half-sibling'),
487            ],
488            'CREM' => [
489                'M' => I18N::translate('Cremation of a half-brother'),
490                'F' => I18N::translate('Cremation of a half-sister'),
491                'U' => I18N::translate('Cremation of a half-sibling'),
492            ],
493        ];
494
495        $death_of_a_grandchild = [
496            'DEAT' => [
497                'M' => I18N::translate('Death of a grandson'),
498                'F' => I18N::translate('Death of a granddaughter'),
499                'U' => I18N::translate('Death of a grandchild'),
500            ],
501            'BURI' => [
502                'M' => I18N::translate('Burial of a grandson'),
503                'F' => I18N::translate('Burial of a granddaughter'),
504                'U' => I18N::translate('Burial of a grandchild'),
505            ],
506            'CREM' => [
507                'M' => I18N::translate('Cremation of a grandson'),
508                'F' => I18N::translate('Cremation of a granddaughter'),
509                'U' => I18N::translate('Baptism of a grandchild'),
510            ],
511        ];
512
513        $death_of_a_grandchild1 = [
514            'DEAT' => [
515                'M' => I18N::translateContext('daughter’s son', 'Death of a grandson'),
516                'F' => I18N::translateContext('daughter’s daughter', 'Death of a granddaughter'),
517                'U' => I18N::translate('Death of a grandchild'),
518            ],
519            'BURI' => [
520                'M' => I18N::translateContext('daughter’s son', 'Burial of a grandson'),
521                'F' => I18N::translateContext('daughter’s daughter', 'Burial of a granddaughter'),
522                'U' => I18N::translate('Burial of a grandchild'),
523            ],
524            'CREM' => [
525                'M' => I18N::translateContext('daughter’s son', 'Cremation of a grandson'),
526                'F' => I18N::translateContext('daughter’s daughter', 'Cremation of a granddaughter'),
527                'U' => I18N::translate('Baptism of a grandchild'),
528            ],
529        ];
530
531        $death_of_a_grandchild2 = [
532            'DEAT' => [
533                'M' => I18N::translateContext('son’s son', 'Death of a grandson'),
534                'F' => I18N::translateContext('son’s daughter', 'Death of a granddaughter'),
535                'U' => I18N::translate('Death of a grandchild'),
536            ],
537            'BURI' => [
538                'M' => I18N::translateContext('son’s son', 'Burial of a grandson'),
539                'F' => I18N::translateContext('son’s daughter', 'Burial of a granddaughter'),
540                'U' => I18N::translate('Burial of a grandchild'),
541            ],
542            'CREM' => [
543                'M' => I18N::translateContext('son’s son', 'Cremation of a grandson'),
544                'F' => I18N::translateContext('son’s daughter', 'Cremation of a granddaughter'),
545                'U' => I18N::translate('Cremation of a grandchild'),
546            ],
547        ];
548
549        $marriage_of_a_child = [
550            'M' => I18N::translate('Marriage of a son'),
551            'F' => I18N::translate('Marriage of a daughter'),
552            'U' => I18N::translate('Marriage of a child'),
553        ];
554
555        $marriage_of_a_grandchild = [
556            'M' => I18N::translate('Marriage of a grandson'),
557            'F' => I18N::translate('Marriage of a granddaughter'),
558            'U' => I18N::translate('Marriage of a grandchild'),
559        ];
560
561        $marriage_of_a_grandchild1 = [
562            'M' => I18N::translateContext('daughter’s son', 'Marriage of a grandson'),
563            'F' => I18N::translateContext('daughter’s daughter', 'Marriage of a granddaughter'),
564            'U' => I18N::translate('Marriage of a grandchild'),
565        ];
566
567        $marriage_of_a_grandchild2 = [
568            'M' => I18N::translateContext('son’s son', 'Marriage of a grandson'),
569            'F' => I18N::translateContext('son’s daughter', 'Marriage of a granddaughter'),
570            'U' => I18N::translate('Marriage of a grandchild'),
571        ];
572
573        $marriage_of_a_sibling = [
574            'M' => I18N::translate('Marriage of a brother'),
575            'F' => I18N::translate('Marriage of a sister'),
576            'U' => I18N::translate('Marriage of a sibling'),
577        ];
578
579        $marriage_of_a_half_sibling = [
580            'M' => I18N::translate('Marriage of a half-brother'),
581            'F' => I18N::translate('Marriage of a half-sister'),
582            'U' => I18N::translate('Marriage of a half-sibling'),
583        ];
584
585        $facts = [];
586
587        // Deal with recursion.
588        switch ($option) {
589            case '_CHIL':
590                // Add grandchildren
591                foreach ($family->children() as $child) {
592                    foreach ($child->spouseFamilies() as $cfamily) {
593                        switch ($child->sex()) {
594                            case 'M':
595                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) {
596                                    $facts[] = $fact;
597                                }
598                                break;
599                            case 'F':
600                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) {
601                                    $facts[] = $fact;
602                                }
603                                break;
604                            default:
605                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) {
606                                    $facts[] = $fact;
607                                }
608                                break;
609                        }
610                    }
611                }
612                break;
613        }
614
615        // For each child in the family
616        foreach ($family->children() as $child) {
617            if ($child->xref() === $person->xref()) {
618                // We are not our own sibling!
619                continue;
620            }
621            // add child’s birth
622            if (str_contains($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option))) {
623                foreach ($child->facts(['BIRT', 'CHR', 'BAPM', 'ADOP']) as $fact) {
624                    // Always show _BIRT_CHIL, even if the dates are not known
625                    if ($option === '_CHIL' || $this->includeFact($fact, $min_date, $max_date)) {
626                        switch ($option) {
627                            case '_GCHI':
628                                switch ($relation) {
629                                    case 'dau':
630                                        $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild1[$fact->getTag()][$fact->record()->sex()]);
631                                        break;
632                                    case 'son':
633                                        $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild2[$fact->getTag()][$fact->record()->sex()]);
634                                        break;
635                                    case 'chil':
636                                        $facts[] = $this->convertEvent($fact, $birth_of_a_grandchild[$fact->getTag()][$fact->record()->sex()]);
637                                        break;
638                                }
639                                break;
640                            case '_SIBL':
641                                $facts[] = $this->convertEvent($fact, $birth_of_a_sibling[$fact->getTag()][$fact->record()->sex()]);
642                                break;
643                            case '_HSIB':
644                                $facts[] = $this->convertEvent($fact, $birth_of_a_half_sibling[$fact->getTag()][$fact->record()->sex()]);
645                                break;
646                            case '_CHIL':
647                                $facts[] = $this->convertEvent($fact, $birth_of_a_child[$fact->getTag()][$fact->record()->sex()]);
648                                break;
649                        }
650                    }
651                }
652            }
653            // add child’s death
654            if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option))) {
655                foreach ($child->facts(['DEAT', 'BURI', 'CREM']) as $fact) {
656                    if ($this->includeFact($fact, $min_date, $max_date)) {
657                        switch ($option) {
658                            case '_GCHI':
659                                switch ($relation) {
660                                    case 'dau':
661                                        $facts[] = $this->convertEvent($fact, $death_of_a_grandchild1[$fact->getTag()][$fact->record()->sex()]);
662                                        break;
663                                    case 'son':
664                                        $facts[] = $this->convertEvent($fact, $death_of_a_grandchild2[$fact->getTag()][$fact->record()->sex()]);
665                                        break;
666                                    case 'chi':
667                                        $facts[] = $this->convertEvent($fact, $death_of_a_grandchild[$fact->getTag()][$fact->record()->sex()]);
668                                        break;
669                                }
670                                break;
671                            case '_SIBL':
672                                $facts[] = $this->convertEvent($fact, $death_of_a_sibling[$fact->getTag()][$fact->record()->sex()]);
673                                break;
674                            case '_HSIB':
675                                $facts[] = $this->convertEvent($fact, $death_of_a_half_sibling[$fact->getTag()][$fact->record()->sex()]);
676                                break;
677                            case '_CHIL':
678                                $facts[] = $this->convertEvent($fact, $death_of_a_child[$fact->getTag()][$fact->record()->sex()]);
679                                break;
680                        }
681                    }
682                }
683            }
684
685            // add child’s marriage
686            if (str_contains($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) {
687                foreach ($child->spouseFamilies() as $sfamily) {
688                    foreach ($sfamily->facts(['MARR']) as $fact) {
689                        if ($this->includeFact($fact, $min_date, $max_date)) {
690                            switch ($option) {
691                                case '_GCHI':
692                                    switch ($relation) {
693                                        case 'dau':
694                                            $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild1['F']);
695                                            break;
696                                        case 'son':
697                                            $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild2['M']);
698                                            break;
699                                        case 'chi':
700                                            $facts[] = $this->convertEvent($fact, $marriage_of_a_grandchild['U']);
701                                            break;
702                                    }
703                                    break;
704                                case '_SIBL':
705                                    $facts[] = $this->convertEvent($fact, $marriage_of_a_sibling['U']);
706                                    break;
707                                case '_HSIB':
708                                    $facts[] = $this->convertEvent($fact, $marriage_of_a_half_sibling['U']);
709                                    break;
710                                case '_CHIL':
711                                    $facts[] = $this->convertEvent($fact, $marriage_of_a_child['U']);
712                                    break;
713                            }
714                        }
715                    }
716                }
717            }
718        }
719
720        return $facts;
721    }
722
723    /**
724     * Get the events of parents and grandparents.
725     *
726     * @param Individual $person
727     * @param int        $sosa
728     * @param Date       $min_date
729     * @param Date       $max_date
730     *
731     * @return Fact[]
732     */
733    private function parentFacts(Individual $person, int $sosa, Date $min_date, Date $max_date): array
734    {
735        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
736
737        $death_of_a_parent = [
738            'DEAT' => [
739                'M' => I18N::translate('Death of a father'),
740                'F' => I18N::translate('Death of a mother'),
741                'U' => I18N::translate('Death of a parent'),
742            ],
743            'BURI' => [
744                'M' => I18N::translate('Burial of a father'),
745                'F' => I18N::translate('Burial of a mother'),
746                'U' => I18N::translate('Burial of a parent'),
747            ],
748            'CREM' => [
749                'M' => I18N::translate('Cremation of a father'),
750                'F' => I18N::translate('Cremation of a mother'),
751                'U' => I18N::translate('Cremation of a parent'),
752            ],
753        ];
754
755        $death_of_a_grandparent = [
756            'DEAT' => [
757                'M' => I18N::translate('Death of a grandfather'),
758                'F' => I18N::translate('Death of a grandmother'),
759                'U' => I18N::translate('Death of a grandparent'),
760            ],
761            'BURI' => [
762                'M' => I18N::translate('Burial of a grandfather'),
763                'F' => I18N::translate('Burial of a grandmother'),
764                'U' => I18N::translate('Burial of a grandparent'),
765            ],
766            'CREM' => [
767                'M' => I18N::translate('Cremation of a grandfather'),
768                'F' => I18N::translate('Cremation of a grandmother'),
769                'U' => I18N::translate('Cremation of a grandparent'),
770            ],
771        ];
772
773        $death_of_a_maternal_grandparent = [
774            'DEAT' => [
775                'M' => I18N::translate('Death of a maternal grandfather'),
776                'F' => I18N::translate('Death of a maternal grandmother'),
777                'U' => I18N::translate('Death of a grandparent'),
778            ],
779            'BURI' => [
780                'M' => I18N::translate('Burial of a maternal grandfather'),
781                'F' => I18N::translate('Burial of a maternal grandmother'),
782                'U' => I18N::translate('Burial of a grandparent'),
783            ],
784            'CREM' => [
785                'M' => I18N::translate('Cremation of a maternal grandfather'),
786                'F' => I18N::translate('Cremation of a maternal grandmother'),
787                'U' => I18N::translate('Cremation of a grandparent'),
788            ],
789        ];
790
791        $death_of_a_paternal_grandparent = [
792            'DEAT' => [
793                'M' => I18N::translate('Death of a paternal grandfather'),
794                'F' => I18N::translate('Death of a paternal grandmother'),
795                'U' => I18N::translate('Death of a grandparent'),
796            ],
797            'BURI' => [
798                'M' => I18N::translate('Burial of a paternal grandfather'),
799                'F' => I18N::translate('Burial of a paternal grandmother'),
800                'U' => I18N::translate('Burial of a grandparent'),
801            ],
802            'CREM' => [
803                'M' => I18N::translate('Cremation of a paternal grandfather'),
804                'F' => I18N::translate('Cremation of a paternal grandmother'),
805                'U' => I18N::translate('Cremation of a grandparent'),
806            ],
807        ];
808
809        $marriage_of_a_parent = [
810            'M' => I18N::translate('Marriage of a father'),
811            'F' => I18N::translate('Marriage of a mother'),
812            'U' => I18N::translate('Marriage of a parent'),
813        ];
814
815        $facts = [];
816
817        if ($sosa === 1) {
818            foreach ($person->childFamilies() as $family) {
819                // Add siblings
820                foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) {
821                    $facts[] = $fact;
822                }
823                foreach ($family->spouses() as $spouse) {
824                    foreach ($spouse->spouseFamilies() as $sfamily) {
825                        if ($family !== $sfamily) {
826                            // Add half-siblings
827                            foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) {
828                                $facts[] = $fact;
829                            }
830                        }
831                    }
832                    // Add grandparents
833                    foreach ($this->parentFacts($spouse, $spouse->sex() === 'F' ? 3 : 2, $min_date, $max_date) as $fact) {
834                        $facts[] = $fact;
835                    }
836                }
837            }
838
839            if (str_contains($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) {
840                // add father/mother marriages
841                foreach ($person->childFamilies() as $sfamily) {
842                    foreach ($sfamily->facts(['MARR']) as $fact) {
843                        if ($this->includeFact($fact, $min_date, $max_date)) {
844                            // marriage of parents (to each other)
845                            $facts[] = $this->convertEvent($fact, I18N::translate('Marriage of parents'));
846                        }
847                    }
848                }
849                foreach ($person->childStepFamilies() as $sfamily) {
850                    foreach ($sfamily->facts(['MARR']) as $fact) {
851                        if ($this->includeFact($fact, $min_date, $max_date)) {
852                            // marriage of a parent (to another spouse)
853                            $facts[] = $this->convertEvent($fact, $marriage_of_a_parent['U']);
854                        }
855                    }
856                }
857            }
858        }
859
860        foreach ($person->childFamilies() as $family) {
861            foreach ($family->spouses() as $parent) {
862                if (str_contains($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa === 1 ? '_PARE' : '_GPAR'))) {
863                    foreach ($parent->facts(['DEAT', 'BURI', 'CREM']) as $fact) {
864                        // Show death of parent when it happened prior to birth
865                        if ($sosa === 1 && Date::compare($fact->date(), $min_date) < 0 || $this->includeFact($fact, $min_date, $max_date)) {
866                            switch ($sosa) {
867                                case 1:
868                                    $facts[] = $this->convertEvent($fact, $death_of_a_parent[$fact->getTag()][$fact->record()->sex()]);
869                                    break;
870                                case 2:
871                                case 3:
872                                    switch ($person->sex()) {
873                                        case 'M':
874                                            $facts[] = $this->convertEvent($fact, $death_of_a_paternal_grandparent[$fact->getTag()][$fact->record()->sex()]);
875                                            break;
876                                        case 'F':
877                                            $facts[] = $this->convertEvent($fact, $death_of_a_maternal_grandparent[$fact->getTag()][$fact->record()->sex()]);
878                                            break;
879                                        default:
880                                            $facts[] = $this->convertEvent($fact, $death_of_a_grandparent[$fact->getTag()][$fact->record()->sex()]);
881                                            break;
882                                    }
883                            }
884                        }
885                    }
886                }
887            }
888        }
889
890        return $facts;
891    }
892
893    /**
894     * Get any historical events.
895     *
896     * @param Individual $individual
897     *
898     * @return Fact[]
899     */
900    private function historicalFacts(Individual $individual): array
901    {
902        return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class)
903            ->map(static function (ModuleHistoricEventsInterface $module) use ($individual): Collection {
904                return $module->historicEventsForIndividual($individual);
905            })
906            ->flatten()
907            ->all();
908    }
909
910    /**
911     * Get the events of associates.
912     *
913     * @param Individual $person
914     *
915     * @return Fact[]
916     */
917    private function associateFacts(Individual $person): array
918    {
919        $facts = [];
920
921        /** @var Individual[] $associates */
922        $asso1 = $person->linkedIndividuals('ASSO');
923        $asso2 = $person->linkedIndividuals('_ASSO');
924        $asso3 = $person->linkedFamilies('ASSO');
925        $asso4 = $person->linkedFamilies('_ASSO');
926
927        $associates = $asso1->merge($asso2)->merge($asso3)->merge($asso4);
928
929        foreach ($associates as $associate) {
930            foreach ($associate->facts() as $fact) {
931                if (preg_match('/\n\d _?ASSO @' . $person->xref() . '@/', $fact->gedcom())) {
932                    // Extract the important details from the fact
933                    $factrec = '1 ' . $fact->getTag();
934                    if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) {
935                        $factrec .= $match[0];
936                    }
937                    if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) {
938                        $factrec .= $match[0];
939                    }
940                    if ($associate instanceof Family) {
941                        foreach ($associate->spouses() as $spouse) {
942                            $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@';
943                        }
944                    } else {
945                        $factrec .= "\n2 _ASSO @" . $associate->xref() . '@';
946                    }
947                    $facts[] = new Fact($factrec, $associate, 'asso');
948                }
949            }
950        }
951
952        return $facts;
953    }
954
955    /**
956     * This module handles the following facts - so don't show them on the "Facts and events" tab.
957     *
958     * @return Collection<string>
959     */
960    public function supportedFacts(): Collection
961    {
962        // We don't actually displaye these facts, but they are displayed
963        // outside the tabs/sidebar systems. This just forces them to be excluded here.
964        return new Collection(['NAME', 'SEX', 'OBJE']);
965    }
966}
967