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