xref: /webtrees/app/Module/IndividualFactsTabModule.php (revision ac5f8ed16774e57c5f89faca528d55bc4a3971e1)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2019 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees\Module;
19
20use Fisharebest\Webtrees\Auth;
21use Fisharebest\Webtrees\Date;
22use Fisharebest\Webtrees\Fact;
23use Fisharebest\Webtrees\Family;
24use Fisharebest\Webtrees\Gedcom;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Individual;
27use Fisharebest\Webtrees\Services\ClipboardService;
28use Fisharebest\Webtrees\Services\ModuleService;
29use Illuminate\Support\Collection;
30
31/**
32 * Class IndividualFactsTabModule
33 */
34class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterface
35{
36    use ModuleTabTrait;
37
38    /** @var ModuleService */
39    private $module_service;
40
41    /** @var ClipboardService */
42    private $clipboard_service;
43
44    /**
45     * UserWelcomeModule constructor.
46     *
47     * @param ModuleService    $module_service
48     * @param ClipboardService $clipboard_service
49     */
50    public function __construct(ModuleService $module_service, ClipboardService $clipboard_service)
51    {
52        $this->module_service    = $module_service;
53        $this->clipboard_service = $clipboard_service;
54    }
55
56    /**
57     * How should this module be identified in the control panel, etc.?
58     *
59     * @return string
60     */
61    public function title(): string
62    {
63        /* I18N: Name of a module/tab on the individual page. */
64        return I18N::translate('Facts and events');
65    }
66
67    /**
68     * A sentence describing what this module does.
69     *
70     * @return string
71     */
72    public function description(): string
73    {
74        /* I18N: Description of the “Facts and events” module */
75        return I18N::translate('A tab showing the facts and events of an individual.');
76    }
77
78    /**
79     * The default position for this tab.  It can be changed in the control panel.
80     *
81     * @return int
82     */
83    public function defaultTabOrder(): int
84    {
85        return 1;
86    }
87
88    /** {@inheritdoc} */
89    public function isGrayedOut(Individual $individual): bool
90    {
91        return false;
92    }
93
94    /** {@inheritdoc} */
95    public function getTabContent(Individual $individual): string
96    {
97        // Only include events of close relatives that are between birth and death
98        $min_date = $individual->getEstimatedBirthDate();
99        $max_date = $individual->getEstimatedDeathDate();
100
101        // Which facts and events are handled by other modules?
102        $sidebar_facts = $this->module_service
103            ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
104            ->map(static function (ModuleSidebarInterface $sidebar): Collection {
105                return $sidebar->supportedFacts();
106            });
107
108        $tab_facts = $this->module_service
109            ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
110            ->map(static function (ModuleTabInterface $sidebar): Collection {
111                return $sidebar->supportedFacts();
112            });
113
114        $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten();
115
116
117        // The individual’s own facts
118        $indifacts = $individual->facts()
119            ->filter(static function (Fact $fact) use ($exclude_facts): bool {
120                return !$exclude_facts->contains($fact->getTag());
121            });
122
123        // Add spouse-family facts
124        foreach ($individual->spouseFamilies() as $family) {
125            foreach ($family->facts() as $fact) {
126                if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') {
127                    $indifacts->push($fact);
128                }
129            }
130
131            $spouse = $family->spouse($individual);
132
133            if ($spouse instanceof Individual) {
134                $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date);
135                $indifacts    = $indifacts->merge($spouse_facts);
136            }
137
138            $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date);
139            $indifacts   = $indifacts->merge($child_facts);
140        }
141
142        $parent_facts     = $this->parentFacts($individual, 1, $min_date, $max_date);
143        $associate_facts  = $this->associateFacts($individual);
144        $historical_facts = $this->historicalFacts($individual);
145
146        $indifacts = $indifacts
147            ->merge($parent_facts)
148            ->merge($associate_facts)
149            ->merge($historical_facts);
150
151        $indifacts = Fact::sortFacts($indifacts);
152
153        return view('modules/personal_facts/tab', [
154            'can_edit'             => $individual->canEdit(),
155            'clipboard_facts'      => $this->clipboard_service->pastableFacts($individual, $exclude_facts),
156            'has_historical_facts' => !empty($historical_facts),
157            'individual'           => $individual,
158            'facts'                => $indifacts,
159        ]);
160    }
161
162    /**
163     * Does a relative event occur within a date range (i.e. the individual's lifetime)?
164     *
165     * @param Fact $fact
166     * @param Date $min_date
167     * @param Date $max_date
168     *
169     * @return bool
170     */
171    private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool
172    {
173        $fact_date = $fact->date();
174
175        return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0;
176    }
177
178    /** {@inheritdoc} */
179    public function hasTabContent(Individual $individual): bool
180    {
181        return true;
182    }
183
184    /** {@inheritdoc} */
185    public function canLoadAjax(): bool
186    {
187        return false;
188    }
189
190    /**
191     * Spouse facts that are shown on an individual’s page.
192     *
193     * @param Individual $individual Show events that occured 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 Fact[]
199     */
200    private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): array
201    {
202        $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS');
203
204        $facts = [];
205        if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) {
206            foreach ($spouse->facts(Gedcom::DEATH_EVENTS) as $fact) {
207                if ($this->includeFact($fact, $min_date, $max_date)) {
208                    // Convert the event to a close relatives event.
209                    $rela_fact = clone $fact;
210                    $rela_fact->setTag('_' . $fact->getTag() . '_SPOU');
211                    $facts[] = $rela_fact;
212                }
213            }
214        }
215
216        return $facts;
217    }
218
219    /**
220     * Get the events of children and grandchildren.
221     *
222     * @param Individual $person
223     * @param Family     $family
224     * @param string     $option
225     * @param string     $relation
226     * @param Date       $min_date
227     * @param Date       $max_date
228     *
229     * @return Fact[]
230     */
231    private function childFacts(Individual $person, Family $family, $option, $relation, Date $min_date, Date $max_date): array
232    {
233        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
234
235        $facts = [];
236
237        // Deal with recursion.
238        switch ($option) {
239            case '_CHIL':
240                // Add grandchildren
241                foreach ($family->children() as $child) {
242                    foreach ($child->spouseFamilies() as $cfamily) {
243                        switch ($child->sex()) {
244                            case 'M':
245                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) {
246                                    $facts[] = $fact;
247                                }
248                                break;
249                            case 'F':
250                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) {
251                                    $facts[] = $fact;
252                                }
253                                break;
254                            default:
255                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) {
256                                    $facts[] = $fact;
257                                }
258                                break;
259                        }
260                    }
261                }
262                break;
263        }
264
265        // For each child in the family
266        foreach ($family->children() as $child) {
267            if ($child->xref() === $person->xref()) {
268                // We are not our own sibling!
269                continue;
270            }
271            // add child’s birth
272            if (strpos($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option)) !== false) {
273                foreach ($child->facts(Gedcom::BIRTH_EVENTS) as $fact) {
274                    // Always show _BIRT_CHIL, even if the dates are not known
275                    if ($option === '_CHIL' || $this->includeFact($fact, $min_date, $max_date)) {
276                        if ($option === '_GCHI' && $relation === 'dau') {
277                            // Convert the event to a close relatives event.
278                            $rela_fact = clone $fact;
279                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH1');
280                            $facts[] = $rela_fact;
281                        } elseif ($option === '_GCHI' && $relation === 'son') {
282                            // Convert the event to a close relatives event.
283                            $rela_fact = clone $fact;
284                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH2');
285                            $facts[] = $rela_fact;
286                        } else {
287                            // Convert the event to a close relatives event.
288                            $rela_fact = clone $fact;
289                            $rela_fact->setTag('_' . $fact->getTag() . $option);
290                            $facts[] = $rela_fact;
291                        }
292                    }
293                }
294            }
295            // add child’s death
296            if (strpos($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option)) !== false) {
297                foreach ($child->facts(Gedcom::DEATH_EVENTS) as $fact) {
298                    if ($this->includeFact($fact, $min_date, $max_date)) {
299                        if ($option === '_GCHI' && $relation === 'dau') {
300                            // Convert the event to a close relatives event.
301                            $rela_fact = clone $fact;
302                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH1');
303                            $facts[] = $rela_fact;
304                        } elseif ($option === '_GCHI' && $relation === 'son') {
305                            // Convert the event to a close relatives event.
306                            $rela_fact = clone $fact;
307                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH2');
308                            $facts[] = $rela_fact;
309                        } else {
310                            // Convert the event to a close relatives event.
311                            $rela_fact = clone $fact;
312                            $rela_fact->setTag('_' . $fact->getTag() . $option);
313                            $facts[] = $rela_fact;
314                        }
315                    }
316                }
317            }
318            // add child’s marriage
319            if (strstr($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) {
320                foreach ($child->spouseFamilies() as $sfamily) {
321                    foreach ($sfamily->facts(['MARR']) as $fact) {
322                        if ($this->includeFact($fact, $min_date, $max_date)) {
323                            if ($option === '_GCHI' && $relation === 'dau') {
324                                // Convert the event to a close relatives event.
325                                $rela_fact = clone $fact;
326                                $rela_fact->setTag('_' . $fact->getTag() . '_GCH1');
327                                $facts[] = $rela_fact;
328                            } elseif ($option === '_GCHI' && $relation === 'son') {
329                                // Convert the event to a close relatives event.
330                                $rela_fact = clone $fact;
331                                $rela_fact->setTag('_' . $fact->getTag() . '_GCH2');
332                                $facts[] = $rela_fact;
333                            } else {
334                                // Convert the event to a close relatives event.
335                                $rela_fact = clone $fact;
336                                $rela_fact->setTag('_' . $fact->getTag() . $option);
337                                $facts[] = $rela_fact;
338                            }
339                        }
340                    }
341                }
342            }
343        }
344
345        return $facts;
346    }
347
348    /**
349     * Get the events of parents and grandparents.
350     *
351     * @param Individual $person
352     * @param int        $sosa
353     * @param Date       $min_date
354     * @param Date       $max_date
355     *
356     * @return Fact[]
357     */
358    private function parentFacts(Individual $person, $sosa, Date $min_date, Date $max_date): array
359    {
360        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
361
362        $facts = [];
363
364        if ($sosa == 1) {
365            foreach ($person->childFamilies() as $family) {
366                // Add siblings
367                foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) {
368                    $facts[] = $fact;
369                }
370                foreach ($family->spouses() as $spouse) {
371                    foreach ($spouse->spouseFamilies() as $sfamily) {
372                        if ($family !== $sfamily) {
373                            // Add half-siblings
374                            foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) {
375                                $facts[] = $fact;
376                            }
377                        }
378                    }
379                    // Add grandparents
380                    foreach ($this->parentFacts($spouse, $spouse->sex() === 'F' ? 3 : 2, $min_date, $max_date) as $fact) {
381                        $facts[] = $fact;
382                    }
383                }
384            }
385
386            if (strstr($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) {
387                // add father/mother marriages
388                foreach ($person->childFamilies() as $sfamily) {
389                    foreach ($sfamily->facts(['MARR']) as $fact) {
390                        if ($this->includeFact($fact, $min_date, $max_date)) {
391                            // marriage of parents (to each other)
392                            $rela_fact = clone $fact;
393                            $rela_fact->setTag('_' . $fact->getTag() . '_FAMC');
394                            $facts[] = $rela_fact;
395                        }
396                    }
397                }
398                foreach ($person->childStepFamilies() as $sfamily) {
399                    foreach ($sfamily->facts(['MARR']) as $fact) {
400                        if ($this->includeFact($fact, $min_date, $max_date)) {
401                            // marriage of a parent (to another spouse)
402                            // Convert the event to a close relatives event
403                            $rela_fact = clone $fact;
404                            $rela_fact->setTag('_' . $fact->getTag() . '_PARE');
405                            $facts[] = $rela_fact;
406                        }
407                    }
408                }
409            }
410        }
411
412        foreach ($person->childFamilies() as $family) {
413            foreach ($family->spouses() as $parent) {
414                if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa == 1 ? '_PARE' : '_GPAR'))) {
415                    foreach ($parent->facts(Gedcom::DEATH_EVENTS) as $fact) {
416                        if ($this->includeFact($fact, $min_date, $max_date)) {
417                            switch ($sosa) {
418                                case 1:
419                                    // Convert the event to a close relatives event.
420                                    $rela_fact = clone $fact;
421                                    $rela_fact->setTag('_' . $fact->getTag() . '_PARE');
422                                    $facts[] = $rela_fact;
423                                    break;
424                                case 2:
425                                    // Convert the event to a close relatives event
426                                    $rela_fact = clone $fact;
427                                    $rela_fact->setTag('_' . $fact->getTag() . '_GPA1');
428                                    $facts[] = $rela_fact;
429                                    break;
430                                case 3:
431                                    // Convert the event to a close relatives event
432                                    $rela_fact = clone $fact;
433                                    $rela_fact->setTag('_' . $fact->getTag() . '_GPA2');
434                                    $facts[] = $rela_fact;
435                                    break;
436                            }
437                        }
438                    }
439                }
440            }
441        }
442
443        return $facts;
444    }
445
446    /**
447     * Get any historical events.
448     *
449     * @param Individual $individual
450     *
451     * @return Fact[]
452     */
453    private function historicalFacts(Individual $individual): array
454    {
455        return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class)
456            ->map(static function (ModuleHistoricEventsInterface $module) use ($individual): Collection {
457                return $module->historicEventsForIndividual($individual);
458            })
459            ->flatten()
460            ->all();
461    }
462
463    /**
464     * Get the events of associates.
465     *
466     * @param Individual $person
467     *
468     * @return Fact[]
469     */
470    private function associateFacts(Individual $person): array
471    {
472        $facts = [];
473
474        /** @var Individual[] $associates */
475        $asso1 = $person->linkedIndividuals('ASSO');
476        $asso2 = $person->linkedIndividuals('_ASSO');
477        $asso3 = $person->linkedFamilies('ASSO');
478        $asso4 = $person->linkedFamilies('_ASSO');
479
480        $associates = $asso1->merge($asso2)->merge($asso3)->merge($asso4);
481
482        foreach ($associates as $associate) {
483            foreach ($associate->facts() as $fact) {
484                if (preg_match('/\n\d _?ASSO @' . $person->xref() . '@/', $fact->gedcom())) {
485                    // Extract the important details from the fact
486                    $factrec = '1 ' . $fact->getTag();
487                    if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) {
488                        $factrec .= $match[0];
489                    }
490                    if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) {
491                        $factrec .= $match[0];
492                    }
493                    if ($associate instanceof Family) {
494                        foreach ($associate->spouses() as $spouse) {
495                            $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@';
496                        }
497                    } else {
498                        $factrec .= "\n2 _ASSO @" . $associate->xref() . '@';
499                    }
500                    $facts[] = new Fact($factrec, $associate, 'asso');
501                }
502            }
503        }
504
505        return $facts;
506    }
507
508    /**
509     * This module handles the following facts - so don't show them on the "Facts and events" tab.
510     *
511     * @return Collection
512     * @return string[]
513     */
514    public function supportedFacts(): Collection
515    {
516        // We don't actually displaye these facts, but they are displayed
517        // outside the tabs/sidebar systems. This just forces them to be excluded here.
518        return new Collection(['NAME', 'SEX']);
519    }
520}
521