Passed
Pull Request — main (#4128)
by David
06:31
created

FunctionsPrint   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 428
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 239
dl 0
loc 428
rs 2
c 3
b 0
f 0
wmc 80

6 Methods

Rating   Name   Duplication   Size   Complexity  
B printAddNewFact() 0 85 5
C formatParentsAges() 0 41 12
C formatFactPlace() 0 58 13
A printFactNotes() 0 25 5
F formatFactDate() 0 116 40
A printNoteRecord() 0 38 5

How to fix   Complexity   

Complex Class

Complex classes like FunctionsPrint often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FunctionsPrint, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Functions;
21
22
use Fisharebest\Webtrees\Age;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Date;
25
use Fisharebest\Webtrees\Fact;
26
use Fisharebest\Webtrees\Family;
27
use Fisharebest\Webtrees\Gedcom;
28
use Fisharebest\Webtrees\GedcomRecord;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Module\ModuleMapLinkInterface;
32
use Fisharebest\Webtrees\Note;
33
use Fisharebest\Webtrees\Place;
34
use Fisharebest\Webtrees\Registry;
35
use Fisharebest\Webtrees\Services\GedcomEditService;
36
use Fisharebest\Webtrees\Services\ModuleService;
37
use Fisharebest\Webtrees\Tree;
38
use Ramsey\Uuid\Uuid;
39
40
use function app;
41
use function array_filter;
42
use function e;
43
use function explode;
44
use function in_array;
45
use function preg_match;
46
use function preg_match_all;
47
use function strlen;
48
use function strpos;
49
use function substr;
50
use function uasort;
51
use function view;
52
53
use const PREG_SET_ORDER;
54
55
/**
56
 * Class FunctionsPrint - common functions
57
 *
58
 * @deprecated since 2.0.6.  Will be removed in 2.1.0
59
 */
60
class FunctionsPrint
61
{
62
    /**
63
     * print a note record
64
     *
65
     * @param Tree   $tree
66
     * @param Note|null $note
67
     * @param string $nrec   the note record to print
68
     *
69
     * @return string
70
     */
71
    private static function printNoteRecord(Tree $tree, $note, string $nrec = ''): string
72
    {
73
        $shared = true;
74
        if ($note === null) {
75
            if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ NOTE/', $nrec, $match)) {
76
                // Shared note.
77
                $note = Registry::noteFactory()->make($match[1], $tree);
78
            } else {
79
                // Inline note, create a dummy note object
80
                $nrec   = '0 @_@ NOTE ' . preg_replace(["/\d (NOTE|CON[C,T]) ?/", "/\r?\n/"], ["", "\n1 CONT "], $nrec);
81
                $note   = Registry::noteFactory()->new('', $nrec, null, $tree);
82
                $shared = false;
83
            }
84
        }
85
        // It must exist.
86
        assert($note instanceof Note);
87
88
        if ($note->canShow()) {
89
            $one_line_only = $note->getNote() === strip_tags(html_entity_decode($note->fullName(), ENT_QUOTES, "UTF-8"));
90
            if ($shared) {
91
                $first_line = '<a href="' . e($note->url()) . '">' . $note->fullName() . '</a>';
92
                $label      = I18N::translate('Shared note');
93
            } else {
94
                $first_line = $note->fullName();
95
                $label      = I18N::translate('Note');
96
            }
97
98
            return view('note', [
99
                'label'         => $label,
100
                'first_line'    => $first_line,
101
                'paragraphs'    => $note->getHtml(),
102
                'one_line_only' => $one_line_only,
103
                'id'            => 'collapse-' . Uuid::uuid4()->toString(),
104
                'expanded'      => (bool) $tree->getPreference('EXPAND_NOTES'),
105
            ]);
106
        }
107
108
        return '';
109
    }
110
111
    /**
112
     * Print all of the notes in this fact record
113
     *
114
     * @param Tree   $tree
115
     * @param string $factrec The fact to print the notes from
116
     * @param int    $level   The level of the notes
117
     *
118
     * @return string HTML
119
     */
120
    public static function printFactNotes(Tree $tree, string $factrec, int $level): string
121
    {
122
        $data          = '';
123
        $previous_spos = 0;
124
        $ct            = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
125
        for ($j = 0; $j < $ct; $j++) {
126
            $spos1 = strpos($factrec, $match[$j][0], $previous_spos);
127
            $spos2 = strpos($factrec . "\n$level", "\n$level", $spos1 + 1);
128
            if (!$spos2) {
129
                $spos2 = strlen($factrec);
130
            }
131
            $previous_spos = $spos2;
132
            $nrec          = substr($factrec, $spos1, $spos2 - $spos1);
133
            if (!isset($match[$j][1])) {
134
                $match[$j][1] = '';
135
            }
136
            if (!preg_match('/^@(' . Gedcom::REGEX_XREF . ')@$/', $match[$j][1], $nmatch)) {
137
                $data .= self::printNoteRecord($tree, null, $nrec);
138
            } else {
139
                $note = Registry::noteFactory()->make($nmatch[1], $tree);
140
                $data    .= self::printNoteRecord($tree, $note);
141
            }
142
        }
143
144
        return $data;
145
    }
146
147
    /**
148
     * Format age of parents in HTML
149
     *
150
     * @param Individual $person child
151
     * @param Date       $birth_date
152
     *
153
     * @return string HTML
154
     */
155
    public static function formatParentsAges(Individual $person, Date $birth_date): string
156
    {
157
        $html     = '';
158
        $families = $person->childFamilies();
159
        // Multiple sets of parents (e.g. adoption) cause complications, so ignore.
160
        if ($birth_date->isOK() && $families->count() === 1) {
161
            $family = $families->first();
162
            foreach ($family->spouses() as $parent) {
163
                if ($parent->getBirthDate()->isOK()) {
164
                    $sex      = '<small>' . view('icons/sex', ['sex' => $parent->sex()]) . '</small>';
165
                    $age      = new Age($parent->getBirthDate(), $birth_date);
166
                    $deatdate = $parent->getDeathDate();
167
                    switch ($parent->sex()) {
168
                        case 'F':
169
                            // Highlight mothers who die in childbirth or shortly afterwards
170
                            if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay() + 90) {
171
                                $html .= ' <span title="' . I18N::translate('Death of a mother') . '" class="parentdeath">' . $sex . I18N::number($age->ageYears()) . '</span>';
172
                            } else {
173
                                $html .= ' <span title="' . I18N::translate('Mother’s age') . '">' . $sex . I18N::number($age->ageYears()) . '</span>';
174
                            }
175
                            break;
176
                        case 'M':
177
                            // Highlight fathers who die before the birth
178
                            if ($deatdate->isOK() && $deatdate->maximumJulianDay() < $birth_date->minimumJulianDay()) {
179
                                $html .= ' <span title="' . I18N::translate('Death of a father') . '" class="parentdeath">' . $sex . I18N::number($age->ageYears()) . '</span>';
180
                            } else {
181
                                $html .= ' <span title="' . I18N::translate('Father’s age') . '">' . $sex . I18N::number($age->ageYears()) . '</span>';
182
                            }
183
                            break;
184
                        default:
185
                            $html .= ' <span title="' . I18N::translate('Parent’s age') . '">' . $sex . I18N::number($age->ageYears()) . '</span>';
186
                            break;
187
                    }
188
                }
189
            }
190
            if ($html) {
191
                $html = '<span class="age">' . $html . '</span>';
192
            }
193
        }
194
195
        return $html;
196
    }
197
198
    /**
199
     * Print fact DATE/TIME
200
     *
201
     * @param Fact         $event  event containing the date/age
202
     * @param GedcomRecord $record the person (or couple) whose ages should be printed
203
     * @param bool         $anchor option to print a link to calendar
204
     * @param bool         $time   option to print TIME value
205
     *
206
     * @return string
207
     */
208
    public static function formatFactDate(Fact $event, GedcomRecord $record, bool $anchor, bool $time): string
209
    {
210
        $element_factory = Registry::elementFactory();
211
212
        $factrec = $event->gedcom();
213
        $html    = '';
214
        // Recorded age
215
        if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
216
            $fact_age = $element_factory->make($event->tag() . ':AGE')->value($match[1], $record->tree());
217
        } else {
218
            $fact_age = '';
219
        }
220
        if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
221
            $husb_age = $element_factory->make($event->tag() . ':HUSB:AGE')->value($match[1], $record->tree());
222
        } else {
223
            $husb_age = '';
224
        }
225
        if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
226
            $wife_age = $element_factory->make($event->tag() . ':WIFE:AGE')->value($match[1], $record->tree());
227
        } else {
228
            $wife_age = '';
229
        }
230
231
        // Calculated age
232
        [, $fact] = explode(':', $event->tag());
233
234
        if (preg_match('/\n2 DATE (.+)/', $factrec, $match)) {
235
            $date = new Date($match[1]);
236
            $html .= ' ' . $date->display($anchor);
237
            // time
238
            if ($time && preg_match('/\n3 TIME (.+)/', $factrec, $match)) {
239
                $html .= ' – <span class="date">' . $match[1] . '</span>';
240
            }
241
            if ($record instanceof Individual) {
242
                if (in_array($fact, Gedcom::BIRTH_EVENTS, true) && $record->tree()->getPreference('SHOW_PARENTS_AGE')) {
243
                    // age of parents at child birth
244
                    $html .= self::formatParentsAges($record, $date);
245
                }
246
                if ($fact !== 'BIRT' && $fact !== 'CHAN' && $fact !== '_TODO') {
247
                    // age at event
248
                    $birth_date = $record->getBirthDate();
249
                    // Can't use getDeathDate(), as this also gives BURI/CREM events, which
250
                    // wouldn't give the correct "days after death" result for people with
251
                    // no DEAT.
252
                    $death_event = $record->facts(['DEAT'])->first();
253
                    if ($death_event instanceof Fact) {
254
                        $death_date = $death_event->date();
255
                    } else {
256
                        $death_date = new Date('');
257
                    }
258
                    $ageText = '';
259
                    if ($fact === 'DEAT' || Date::compare($date, $death_date) <= 0 || !$record->isDead()) {
260
                        // Before death, print age
261
                        $age = (string) new Age($birth_date, $date);
262
263
                        // Only show calculated age if it differs from recorded age
264
                        if ($age !== '') {
265
                            if (
266
                                $fact_age !== '' && !str_starts_with($fact_age, $age) ||
267
                                $fact_age === '' && $husb_age === '' && $wife_age === '' ||
268
                                $husb_age !== '' && !str_starts_with($husb_age, $age) && $record->sex() === 'M' ||
269
                                $wife_age !== '' && !str_starts_with($wife_age, $age) && $record->sex() === 'F'
270
                            ) {
271
                                switch ($record->sex()) {
272
                                    case 'M':
273
                                        /* I18N: The age of an individual at a given date */
274
                                        $ageText = I18N::translateContext('Male', '(aged %s)', $age);
275
                                        break;
276
                                    case 'F':
277
                                        /* I18N: The age of an individual at a given date */
278
                                        $ageText = I18N::translateContext('Female', '(aged %s)', $age);
279
                                        break;
280
                                    default:
281
                                        /* I18N: The age of an individual at a given date */
282
                                        $ageText = I18N::translate('(aged %s)', $age);
283
                                        break;
284
                                }
285
                            }
286
                        }
287
                    }
288
                    if ($fact !== 'DEAT' && $death_date->isOK() && Date::compare($death_date, $date) <= 0) {
289
                        $death_day = $death_date->minimumDate()->day();
290
                        $event_day = $date->minimumDate()->day();
291
                        if ($death_day !== 0 && $event_day !== 0 && Date::compare($death_date, $date) === 0) {
292
                            // On the exact date of death?
293
                            // NOTE: this path is never reached.  Keep the code (translation) in case
294
                            // we decide to re-introduce it.
295
                            $ageText = I18N::translate('(on the date of death)');
296
                        } else {
297
                            // After death
298
                            $age = (string) new Age($death_date, $date);
299
                            $ageText = I18N::translate('(%s after death)', $age);
300
                        }
301
                        // Family events which occur after death are probably errors
302
                        if ($event->record() instanceof Family) {
303
                            $ageText .= view('icons/warning');
304
                        }
305
                    }
306
                    if ($ageText !== '') {
307
                        $html .= ' <span class="age">' . $ageText . '</span>';
308
                    }
309
                }
310
            }
311
        }
312
        // print gedcom ages
313
        $age_labels = [
314
            I18N::translate('Age')     => $fact_age,
315
            I18N::translate('Husband') => $husb_age,
316
            I18N::translate('Wife')    => $wife_age,
317
        ];
318
319
        foreach (array_filter($age_labels) as $label => $age) {
320
            $html .= ' <span class="label">' . $label . ':</span> <span class="age">' . $age . '</span>';
321
        }
322
323
        return $html;
324
    }
325
326
    /**
327
     * print fact PLACe TEMPle STATus
328
     *
329
     * @param Fact $event       gedcom fact record
330
     * @param bool $anchor      to print a link to placelist
331
     * @param bool $sub_records to print place subrecords
332
     * @param bool $lds         to print LDS TEMPle and STATus
333
     *
334
     * @return string HTML
335
     */
336
    public static function formatFactPlace(Fact $event, bool $anchor, bool $sub_records, bool $lds): string
337
    {
338
        $tree = $event->record()->tree();
339
340
        if ($anchor) {
341
            // Show the full place name, for facts/events tab
342
            $html = $event->place()->fullName(true);
343
        } else {
344
            // Abbreviate the place name, for chart boxes
345
            return $event->place()->shortName();
346
        }
347
348
        if ($sub_records) {
349
            $placerec = Functions::getSubRecord(2, '2 PLAC', $event->gedcom());
350
            if ($placerec !== '') {
351
                if (preg_match_all('/\n3 (?:_HEB|ROMN) (.+)/', $placerec, $matches)) {
352
                    foreach ($matches[1] as $match) {
353
                        $wt_place = new Place($match, $tree);
354
                        $html     .= ' - ' . $wt_place->fullName();
355
                    }
356
                }
357
358
                $latitude  = $event->latitude();
359
                $longitude = $event->longitude();
360
361
                if ($latitude !== null && $longitude !== null) {
362
                    $html .= '<br><span class="label">' . I18N::translate('Latitude') . ': </span>' . $latitude;
363
                    $html .= ' <span class="label">' . I18N::translate('Longitude') . ': </span>' . $longitude;
364
365
                    // Links to external maps
366
                    $html .= app(ModuleService::class)
367
                        ->findByInterface(ModuleMapLinkInterface::class)
368
                        ->map(fn (ModuleMapLinkInterface $module): string => ' ' . $module->mapLink($event))
369
                        ->implode('');
370
                }
371
372
                if (preg_match('/\d NOTE (.*)/', $placerec, $match)) {
373
                    $html .= '<br>' . self::printFactNotes($tree, $placerec, 3);
374
                }
375
            }
376
        }
377
        if ($lds) {
378
            if (preg_match('/2 TEMP (.*)/', $event->gedcom(), $match)) {
379
                $element = Registry::elementFactory()->make($event->tag() . ':TEMP');
380
                $html .= $element->labelValue($match[1], $tree);
381
            }
382
            if (preg_match('/2 STAT (.*)/', $event->gedcom(), $match)) {
383
                $element = Registry::elementFactory()->make($event->tag() . ':STAT');
384
                $html .= $element->labelValue($match[1], $tree);
385
                if (preg_match('/3 DATE (.*)/', $event->gedcom(), $match)) {
386
                    $date = new Date($match[1]);
387
                    $element = Registry::elementFactory()->make($event->tag() . ':STAT:DATE');
388
                    $html .= $element->labelValue($date->display(), $tree);
389
                }
390
            }
391
        }
392
393
        return $html;
394
    }
395
396
    /**
397
     * Print a new fact box on details pages
398
     *
399
     * @param Individual|Family $record
400
     *
401
     * @return void
402
     */
403
    public static function printAddNewFact(GedcomRecord $record): void
404
    {
405
        $tree = $record->tree();
406
407
        $add_facts = (new GedcomEditService())->factsToAdd($record, false);
408
409
        // Add from pick list
410
        switch ($record->tag()) {
411
            case Individual::RECORD_TYPE:
412
                $quick_facts  = explode(',', $tree->getPreference('INDI_FACTS_QUICK'));
413
                $unique_facts = [
414
                    'ADOP',
415
                    'AFN',
416
                    'BAPL',
417
                    'BAPM',
418
                    'BARM',
419
                    'BASM',
420
                    'BIRT',
421
                    'BURI',
422
                    'CAST',
423
                    'CHAN',
424
                    'CHR',
425
                    'CHRA',
426
                    'CONF',
427
                    'CONL',
428
                    'CREM',
429
                    'DEAT',
430
                    'ENDL',
431
                    'FCOM',
432
                    'GRAD',
433
                    'NCHI',
434
                    'NMR',
435
                    'ORDN',
436
                    'PROB',
437
                    'REFN',
438
                    'RELI',
439
                    'RESN',
440
                    'RETI',
441
                    'RFN',
442
                    'RIN',
443
                    'SEX',
444
                    'SLGC',
445
                    'SSN',
446
                    'WILL',
447
                ];
448
                break;
449
450
            case Family::RECORD_TYPE:
451
                $quick_facts  = explode(',', $tree->getPreference('FAM_FACTS_QUICK'));
452
                $unique_facts = [
453
                    'DIV',
454
                    'DIVF',
455
                    'ENGA',
456
                    'MARR',
457
                ];
458
                break;
459
460
            default:
461
                $quick_facts  = [];
462
                $unique_facts = [];
463
                break;
464
        }
465
466
        // Filter existing tags
467
        $filter_fn = static fn (string $tag): bool => !in_array($tag, $unique_facts, true) || $record->facts([$tag])->isEmpty();
468
469
        $quick_facts = array_filter($quick_facts, $filter_fn);
470
471
472
        // Create a label for a subtag
473
        $label_fn = static fn (string $subtag): string => Registry::elementFactory()->make($record->tag() . ':' . $subtag)->label();
474
475
        $quick_facts = array_combine($quick_facts, array_map($label_fn, $quick_facts));
476
        $add_facts   = array_combine($add_facts, array_map($label_fn, $add_facts));
477
478
        uasort($add_facts, I18N::comparator());
479
480
        if ((int) $record->tree()->getPreference('MEDIA_UPLOAD') < Auth::accessLevel($record->tree())) {
481
            unset($add_facts['OBJE'], $quick_facts['OBJE']);
482
        }
483
484
        echo view('edit/add-fact-row', [
485
            'add_facts'   => $add_facts,
486
            'quick_facts' => $quick_facts,
487
            'record'      => $record,
488
        ]);
489
    }
490
}
491