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