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