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