xref: /webtrees/app/Module/IndividualFactsTabModule.php (revision 04fe0783f2203f5ce4d65835e61cb4636d80dd39)
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    /**
89     * A greyed out tab has no actual content, but may perhaps have
90     * options to create content.
91     *
92     * @param Individual $individual
93     *
94     * @return bool
95     */
96    public function isGrayedOut(Individual $individual): bool
97    {
98        return false;
99    }
100
101    /**
102     * Generate the HTML content of this tab.
103     *
104     * @param Individual $individual
105     *
106     * @return string
107     */
108    public function getTabContent(Individual $individual): string
109    {
110        // Only include events of close relatives that are between birth and death
111        $min_date = $individual->getEstimatedBirthDate();
112        $max_date = $individual->getEstimatedDeathDate();
113
114        // Which facts and events are handled by other modules?
115        $sidebar_facts = $this->module_service
116            ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
117            ->map(static function (ModuleSidebarInterface $sidebar): Collection {
118                return $sidebar->supportedFacts();
119            });
120
121        $tab_facts = $this->module_service
122            ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
123            ->map(static function (ModuleTabInterface $sidebar): Collection {
124                return $sidebar->supportedFacts();
125            });
126
127        $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten();
128
129
130        // The individual’s own facts
131        $indifacts = $individual->facts()
132            ->filter(static function (Fact $fact) use ($exclude_facts): bool {
133                return !$exclude_facts->contains($fact->getTag());
134            });
135
136        // Add spouse-family facts
137        foreach ($individual->spouseFamilies() as $family) {
138            foreach ($family->facts() as $fact) {
139                if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') {
140                    $indifacts->push($fact);
141                }
142            }
143
144            $spouse = $family->spouse($individual);
145
146            if ($spouse instanceof Individual) {
147                $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date);
148                $indifacts    = $indifacts->merge($spouse_facts);
149            }
150
151            $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date);
152            $indifacts   = $indifacts->merge($child_facts);
153        }
154
155        $parent_facts     = $this->parentFacts($individual, 1, $min_date, $max_date);
156        $associate_facts  = $this->associateFacts($individual);
157        $historical_facts = $this->historicalFacts($individual);
158
159        $indifacts = $indifacts
160            ->merge($parent_facts)
161            ->merge($associate_facts)
162            ->merge($historical_facts);
163
164        $indifacts = Fact::sortFacts($indifacts);
165
166        return view('modules/personal_facts/tab', [
167            'can_edit'             => $individual->canEdit(),
168            'clipboard_facts'      => $this->clipboard_service->pastableFacts($individual, $exclude_facts),
169            'has_historical_facts' => !empty($historical_facts),
170            'individual'           => $individual,
171            'facts'                => $indifacts,
172        ]);
173    }
174
175    /**
176     * Does a relative event occur within a date range (i.e. the individual's lifetime)?
177     *
178     * @param Fact $fact
179     * @param Date $min_date
180     * @param Date $max_date
181     *
182     * @return bool
183     */
184    private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool
185    {
186        $fact_date = $fact->date();
187
188        return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0;
189    }
190
191    /**
192     * Is this tab empty? If so, we don't always need to display it.
193     *
194     * @param Individual $individual
195     *
196     * @return bool
197     */
198    public function hasTabContent(Individual $individual): bool
199    {
200        return true;
201    }
202
203    /**
204     * Can this tab load asynchronously?
205     *
206     * @return bool
207     */
208    public function canLoadAjax(): bool
209    {
210        return false;
211    }
212
213    /**
214     * Spouse facts that are shown on an individual’s page.
215     *
216     * @param Individual $individual Show events that occured during the lifetime of this individual
217     * @param Individual $spouse     Show events of this individual
218     * @param Date       $min_date
219     * @param Date       $max_date
220     *
221     * @return Fact[]
222     */
223    private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): array
224    {
225        $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS');
226
227        $facts = [];
228        if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) {
229            foreach ($spouse->facts(Gedcom::DEATH_EVENTS) as $fact) {
230                if ($this->includeFact($fact, $min_date, $max_date)) {
231                    // Convert the event to a close relatives event.
232                    $rela_fact = clone $fact;
233                    $rela_fact->setTag('_' . $fact->getTag() . '_SPOU');
234                    $facts[] = $rela_fact;
235                }
236            }
237        }
238
239        return $facts;
240    }
241
242    /**
243     * Get the events of children and grandchildren.
244     *
245     * @param Individual $person
246     * @param Family     $family
247     * @param string     $option
248     * @param string     $relation
249     * @param Date       $min_date
250     * @param Date       $max_date
251     *
252     * @return Fact[]
253     */
254    private function childFacts(Individual $person, Family $family, $option, $relation, Date $min_date, Date $max_date): array
255    {
256        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
257
258        $facts = [];
259
260        // Deal with recursion.
261        switch ($option) {
262            case '_CHIL':
263                // Add grandchildren
264                foreach ($family->children() as $child) {
265                    foreach ($child->spouseFamilies() as $cfamily) {
266                        switch ($child->sex()) {
267                            case 'M':
268                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) {
269                                    $facts[] = $fact;
270                                }
271                                break;
272                            case 'F':
273                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) {
274                                    $facts[] = $fact;
275                                }
276                                break;
277                            default:
278                                foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) {
279                                    $facts[] = $fact;
280                                }
281                                break;
282                        }
283                    }
284                }
285                break;
286        }
287
288        // For each child in the family
289        foreach ($family->children() as $child) {
290            if ($child->xref() === $person->xref()) {
291                // We are not our own sibling!
292                continue;
293            }
294            // add child’s birth
295            if (strpos($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option)) !== false) {
296                foreach ($child->facts(Gedcom::BIRTH_EVENTS) as $fact) {
297                    // Always show _BIRT_CHIL, even if the dates are not known
298                    if ($option === '_CHIL' || $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 death
319            if (strpos($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option)) !== false) {
320                foreach ($child->facts(Gedcom::DEATH_EVENTS) as $fact) {
321                    if ($this->includeFact($fact, $min_date, $max_date)) {
322                        if ($option === '_GCHI' && $relation === 'dau') {
323                            // Convert the event to a close relatives event.
324                            $rela_fact = clone $fact;
325                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH1');
326                            $facts[] = $rela_fact;
327                        } elseif ($option === '_GCHI' && $relation === 'son') {
328                            // Convert the event to a close relatives event.
329                            $rela_fact = clone $fact;
330                            $rela_fact->setTag('_' . $fact->getTag() . '_GCH2');
331                            $facts[] = $rela_fact;
332                        } else {
333                            // Convert the event to a close relatives event.
334                            $rela_fact = clone $fact;
335                            $rela_fact->setTag('_' . $fact->getTag() . $option);
336                            $facts[] = $rela_fact;
337                        }
338                    }
339                }
340            }
341            // add child’s marriage
342            if (strstr($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) {
343                foreach ($child->spouseFamilies() as $sfamily) {
344                    foreach ($sfamily->facts(['MARR']) as $fact) {
345                        if ($this->includeFact($fact, $min_date, $max_date)) {
346                            if ($option === '_GCHI' && $relation === 'dau') {
347                                // Convert the event to a close relatives event.
348                                $rela_fact = clone $fact;
349                                $rela_fact->setTag('_' . $fact->getTag() . '_GCH1');
350                                $facts[] = $rela_fact;
351                            } elseif ($option === '_GCHI' && $relation === 'son') {
352                                // Convert the event to a close relatives event.
353                                $rela_fact = clone $fact;
354                                $rela_fact->setTag('_' . $fact->getTag() . '_GCH2');
355                                $facts[] = $rela_fact;
356                            } else {
357                                // Convert the event to a close relatives event.
358                                $rela_fact = clone $fact;
359                                $rela_fact->setTag('_' . $fact->getTag() . $option);
360                                $facts[] = $rela_fact;
361                            }
362                        }
363                    }
364                }
365            }
366        }
367
368        return $facts;
369    }
370
371    /**
372     * Get the events of parents and grandparents.
373     *
374     * @param Individual $person
375     * @param int        $sosa
376     * @param Date       $min_date
377     * @param Date       $max_date
378     *
379     * @return Fact[]
380     */
381    private function parentFacts(Individual $person, $sosa, Date $min_date, Date $max_date): array
382    {
383        $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS');
384
385        $facts = [];
386
387        if ($sosa == 1) {
388            foreach ($person->childFamilies() as $family) {
389                // Add siblings
390                foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) {
391                    $facts[] = $fact;
392                }
393                foreach ($family->spouses() as $spouse) {
394                    foreach ($spouse->spouseFamilies() as $sfamily) {
395                        if ($family !== $sfamily) {
396                            // Add half-siblings
397                            foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) {
398                                $facts[] = $fact;
399                            }
400                        }
401                    }
402                    // Add grandparents
403                    foreach ($this->parentFacts($spouse, $spouse->sex() === 'F' ? 3 : 2, $min_date, $max_date) as $fact) {
404                        $facts[] = $fact;
405                    }
406                }
407            }
408
409            if (strstr($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) {
410                // add father/mother marriages
411                foreach ($person->childFamilies() as $sfamily) {
412                    foreach ($sfamily->facts(['MARR']) as $fact) {
413                        if ($this->includeFact($fact, $min_date, $max_date)) {
414                            // marriage of parents (to each other)
415                            $rela_fact = clone $fact;
416                            $rela_fact->setTag('_' . $fact->getTag() . '_FAMC');
417                            $facts[] = $rela_fact;
418                        }
419                    }
420                }
421                foreach ($person->childStepFamilies() as $sfamily) {
422                    foreach ($sfamily->facts(['MARR']) as $fact) {
423                        if ($this->includeFact($fact, $min_date, $max_date)) {
424                            // marriage of a parent (to another spouse)
425                            // Convert the event to a close relatives event
426                            $rela_fact = clone $fact;
427                            $rela_fact->setTag('_' . $fact->getTag() . '_PARE');
428                            $facts[] = $rela_fact;
429                        }
430                    }
431                }
432            }
433        }
434
435        foreach ($person->childFamilies() as $family) {
436            foreach ($family->spouses() as $parent) {
437                if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa == 1 ? '_PARE' : '_GPAR'))) {
438                    foreach ($parent->facts(Gedcom::DEATH_EVENTS) as $fact) {
439                        if ($this->includeFact($fact, $min_date, $max_date)) {
440                            switch ($sosa) {
441                                case 1:
442                                    // Convert the event to a close relatives event.
443                                    $rela_fact = clone $fact;
444                                    $rela_fact->setTag('_' . $fact->getTag() . '_PARE');
445                                    $facts[] = $rela_fact;
446                                    break;
447                                case 2:
448                                    // Convert the event to a close relatives event
449                                    $rela_fact = clone $fact;
450                                    $rela_fact->setTag('_' . $fact->getTag() . '_GPA1');
451                                    $facts[] = $rela_fact;
452                                    break;
453                                case 3:
454                                    // Convert the event to a close relatives event
455                                    $rela_fact = clone $fact;
456                                    $rela_fact->setTag('_' . $fact->getTag() . '_GPA2');
457                                    $facts[] = $rela_fact;
458                                    break;
459                            }
460                        }
461                    }
462                }
463            }
464        }
465
466        return $facts;
467    }
468
469    /**
470     * Get any historical events.
471     *
472     * @param Individual $individual
473     *
474     * @return Fact[]
475     */
476    private function historicalFacts(Individual $individual): array
477    {
478        return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class)
479            ->map(static function (ModuleHistoricEventsInterface $module) use ($individual): Collection {
480                return $module->historicEventsForIndividual($individual);
481            })
482            ->flatten()
483            ->all();
484    }
485
486    /**
487     * Get the events of associates.
488     *
489     * @param Individual $person
490     *
491     * @return Fact[]
492     */
493    private function associateFacts(Individual $person): array
494    {
495        $facts = [];
496
497        /** @var Individual[] $associates */
498        $asso1 = $person->linkedIndividuals('ASSO');
499        $asso2 = $person->linkedIndividuals('_ASSO');
500        $asso3 = $person->linkedFamilies('ASSO');
501        $asso4 = $person->linkedFamilies('_ASSO');
502
503        $associates = $asso1->merge($asso2)->merge($asso3)->merge($asso4);
504
505        foreach ($associates as $associate) {
506            foreach ($associate->facts() as $fact) {
507                if (preg_match('/\n\d _?ASSO @' . $person->xref() . '@/', $fact->gedcom())) {
508                    // Extract the important details from the fact
509                    $factrec = '1 ' . $fact->getTag();
510                    if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) {
511                        $factrec .= $match[0];
512                    }
513                    if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) {
514                        $factrec .= $match[0];
515                    }
516                    if ($associate instanceof Family) {
517                        foreach ($associate->spouses() as $spouse) {
518                            $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@';
519                        }
520                    } else {
521                        $factrec .= "\n2 _ASSO @" . $associate->xref() . '@';
522                    }
523                    $facts[] = new Fact($factrec, $associate, 'asso');
524                }
525            }
526        }
527
528        return $facts;
529    }
530
531    /**
532     * This module handles the following facts - so don't show them on the "Facts and events" tab.
533     *
534     * @return Collection
535     */
536    public function supportedFacts(): Collection
537    {
538        // We don't actually displaye these facts, but they are displayed
539        // outside the tabs/sidebar systems. This just forces them to be excluded here.
540        return new Collection(['NAME', 'SEX']);
541    }
542}
543