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