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