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