TreeView::drawPerson()   F
last analyzed

Complexity

Conditions 33
Paths > 20000

Size

Total Lines 102
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 60
nc 86017
nop 6
dl 0
loc 102
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 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\Module\InteractiveTree;
21
22
use Fisharebest\Webtrees\Registry;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\Gedcom;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Tree;
28
use Illuminate\Support\Collection;
29
30
/**
31
 * Class TreeView
32
 */
33
class TreeView
34
{
35
    /** @var string HTML element name */
36
    private $name;
37
38
    /**
39
     * Treeview Constructor
40
     *
41
     * @param string $name the name of the TreeView object’s instance
42
     */
43
    public function __construct(string $name = 'tree')
44
    {
45
        $this->name = $name;
46
    }
47
48
    /**
49
     * Draw the viewport which creates the draggable/zoomable framework
50
     * Size is set by the container, as the viewport can scale itself automatically
51
     *
52
     * @param Individual $individual  Draw the chart for this individual
53
     * @param int        $generations number of generations to draw
54
     *
55
     * @return array<string>  HTML and Javascript
56
     */
57
    public function drawViewport(Individual $individual, int $generations): array
58
    {
59
        $html = view('modules/interactive-tree/chart', [
60
            'module'     => 'tree',
61
            'name'       => $this->name,
62
            'individual' => $this->drawPerson($individual, $generations, 0, null, '', true),
63
            'tree'       => $individual->tree(),
64
        ]);
65
66
        return [
67
            $html,
68
            'var ' . $this->name . 'Handler = new TreeViewHandler("' . $this->name . '", "' . e($individual->tree()->name()) . '");',
69
        ];
70
    }
71
72
    /**
73
     * Return a JSON structure to a JSON request
74
     *
75
     * @param Tree   $tree
76
     * @param string $request list of JSON requests
77
     *
78
     * @return string
79
     */
80
    public function getIndividuals(Tree $tree, string $request): string
81
    {
82
        $json_requests = explode(';', $request);
83
        $r             = [];
84
85
        foreach ($json_requests as $json_request) {
86
            $firstLetter = substr($json_request, 0, 1);
87
            $json_request = substr($json_request, 1);
88
89
            switch ($firstLetter) {
90
                case 'c':
91
                    $families = Collection::make(explode(',', $json_request))
92
                        ->map(static function (string $xref) use ($tree): ?Family {
93
                            return Registry::familyFactory()->make($xref, $tree);
94
                        })
95
                        ->filter();
96
97
                    $r[] = $this->drawChildren($families, 1, true);
98
                    break;
99
100
                case 'p':
101
                    [$xref, $order] = explode('@', $json_request);
102
103
                    $family = Registry::familyFactory()->make($xref, $tree);
104
                    if ($family instanceof Family) {
105
                        // Prefer the paternal line
106
                        $parent = $family->husband() ?? $family->wife();
107
108
                        // The family may have no parents (just children).
109
                        if ($parent instanceof Individual) {
110
                            $r[] = $this->drawPerson($parent, 0, 1, $family, $order, false);
111
                        }
112
                    }
113
                    break;
114
            }
115
        }
116
117
        return json_encode($r);
118
    }
119
120
    /**
121
     * Get the details for a person and their life partner(s)
122
     *
123
     * @param Individual $individual the individual to return the details for
124
     *
125
     * @return string
126
     */
127
    public function getDetails(Individual $individual): string
128
    {
129
        $html = $this->getPersonDetails($individual, null);
130
        foreach ($individual->spouseFamilies() as $family) {
131
            $spouse = $family->spouse($individual);
132
            if ($spouse) {
133
                $html .= $this->getPersonDetails($spouse, $family);
134
            }
135
        }
136
137
        return $html;
138
    }
139
140
    /**
141
     * Return the details for a person
142
     *
143
     * @param Individual  $individual
144
     * @param Family|null $family
145
     *
146
     * @return string
147
     */
148
    private function getPersonDetails(Individual $individual, Family $family = null): string
149
    {
150
        $chart_url = route('module', [
151
            'module' => 'tree',
152
            'action' => 'Chart',
153
            'xref'   => $individual->xref(),
154
            'tree'   => $individual->tree()->name(),
155
        ]);
156
157
        $hmtl = $this->getThumbnail($individual);
158
        $hmtl .= '<a class="tv_link" href="' . e($individual->url()) . '">' . $individual->fullName() . '</a> <a href="' . e($chart_url) . '" title="' . I18N::translate('Interactive tree of %s', strip_tags($individual->fullName())) . '" class="tv_link tv_treelink">' . view('icons/individual') . '</a>';
159
        foreach ($individual->facts(Gedcom::BIRTH_EVENTS, true) as $fact) {
160
            $hmtl .= $fact->summary();
161
        }
162
        if ($family) {
163
            foreach ($family->facts(Gedcom::MARRIAGE_EVENTS, true) as $fact) {
164
                $hmtl .= $fact->summary();
165
            }
166
        }
167
        foreach ($individual->facts(Gedcom::DEATH_EVENTS, true) as $fact) {
168
            $hmtl .= $fact->summary();
169
        }
170
171
        return '<div class="tv' . $individual->sex() . ' tv_person_expanded">' . $hmtl . '</div>';
172
    }
173
174
    /**
175
     * Draw the children for some families
176
     *
177
     * @param Collection<Family> $familyList array of families to draw the children for
178
     * @param int                $gen        number of generations to draw
179
     * @param bool               $ajax       true for an ajax call
180
     *
181
     * @return string
182
     */
183
    private function drawChildren(Collection $familyList, int $gen = 1, bool $ajax = false): string
184
    {
185
        $html          = '';
186
        $children2draw = [];
187
        $f2load        = [];
188
189
        foreach ($familyList as $f) {
190
            $children = $f->children();
191
            if ($children->isNotEmpty()) {
192
                $f2load[] = $f->xref();
193
                foreach ($children as $child) {
194
                    // Eliminate duplicates - e.g. when adopted by a step-parent
195
                    $children2draw[$child->xref()] = $child;
196
                }
197
            }
198
        }
199
        $tc = count($children2draw);
200
        if ($tc) {
201
            $f2load = implode(',', $f2load);
202
            $nbc    = 0;
203
            foreach ($children2draw as $child) {
204
                $nbc++;
205
                if ($tc == 1) {
206
                    $co = 'c'; // unique
207
                } elseif ($nbc == 1) {
208
                    $co = 't'; // first
209
                } elseif ($nbc == $tc) {
210
                    $co = 'b'; //last
211
                } else {
212
                    $co = 'h';
213
                }
214
                $html .= $this->drawPerson($child, $gen - 1, -1, null, $co, false);
215
            }
216
            if (!$ajax) {
217
                $html = '<td align="right"' . ($gen == 0 ? ' abbr="c' . $f2load . '"' : '') . '>' . $html . '</td>' . $this->drawHorizontalLine();
218
            }
219
        }
220
221
        return $html;
222
    }
223
224
    /**
225
     * Draw a person in the tree
226
     *
227
     * @param Individual  $person The Person object to draw the box for
228
     * @param int         $gen    The number of generations up or down to print
229
     * @param int         $state  Whether we are going up or down the tree, -1 for descendents +1 for ancestors
230
     * @param Family|null $pfamily
231
     * @param string      $line   b, c, h, t. Required for drawing lines between boxes
232
     * @param bool        $isRoot
233
     *
234
     * @return string
235
     */
236
    private function drawPerson(Individual $person, int $gen, int $state, Family $pfamily = null, string $line = '', bool $isRoot = false): string
237
    {
238
        if ($gen < 0) {
239
            return '';
240
        }
241
242
        if ($pfamily instanceof Family) {
243
            $partner = $pfamily->spouse($person);
244
        } else {
245
            $partner = $person->getCurrentSpouse();
246
        }
247
248
        if ($isRoot) {
249
            $html = '<table id="tvTreeBorder" class="tv_tree"><tbody><tr><td id="tv_tree_topleft"></td><td id="tv_tree_top"></td><td id="tv_tree_topright"></td></tr><tr><td id="tv_tree_left"></td><td>';
250
        } else {
251
            $html = '';
252
        }
253
        /* height 1% : this hack enable the div auto-dimensioning in td for FF & Chrome */
254
        $html .= '<table class="tv_tree"' . ($isRoot ? ' id="tv_tree"' : '') . ' style="height: 1%"><tbody><tr>';
255
256
        if ($state <= 0) {
257
            // draw children
258
            $html .= $this->drawChildren($person->spouseFamilies(), $gen);
259
        } else {
260
            // draw the parent’s lines
261
            $html .= $this->drawVerticalLine($line) . $this->drawHorizontalLine();
262
        }
263
264
        /* draw the person. Do NOT add person or family id as an id, since a same person could appear more than once in the tree !!! */
265
        // Fixing the width for td to the box initial width when the person is the root person fix a rare bug that happen when a person without child and without known parents is the root person : an unwanted white rectangle appear at the right of the person’s boxes, otherwise.
266
        $html .= '<td' . ($isRoot ? ' style="width:1px"' : '') . '><div class="tv_box' . ($isRoot ? ' rootPerson' : '') . '" dir="' . I18N::direction() . '" style="text-align: ' . (I18N::direction() === 'rtl' ? 'right' : 'left') . '; direction: ' . I18N::direction() . '" abbr="' . $person->xref() . '" onclick="' . $this->name . 'Handler.expandBox(this, event);">';
267
        $html .= $this->drawPersonName($person, '');
268
269
        $fop = []; // $fop is fathers of partners
270
271
        if ($partner !== null) {
272
            $dashed = '';
273
            foreach ($person->spouseFamilies() as $family) {
274
                $spouse = $family->spouse($person);
275
                if ($spouse instanceof Individual) {
276
                    $spouse_parents = $spouse->childFamilies()->first();
277
                    if ($spouse_parents instanceof Family) {
278
                        $spouse_parent = $spouse_parents->husband() ?? $spouse_parents->wife();
279
280
                        if ($spouse_parent instanceof Individual) {
281
                            $fop[] = [$spouse_parent, $spouse_parents];
282
                        }
283
                    }
284
285
                    $html .= $this->drawPersonName($spouse, $dashed);
286
                    $dashed = 'dashed';
287
                }
288
            }
289
        }
290
        $html .= '</div></td>';
291
292
        $primaryChildFamily = $person->childFamilies()->first();
293
        if ($primaryChildFamily instanceof Family) {
294
            $parent = $primaryChildFamily->husband() ?? $primaryChildFamily->wife();
295
        } else {
296
            $parent = null;
297
        }
298
299
        if ($parent instanceof Individual || !empty($fop) || $state < 0) {
300
            $html .= $this->drawHorizontalLine();
301
        }
302
303
        /* draw the parents */
304
        if ($state >= 0 && ($parent instanceof Individual || !empty($fop))) {
305
            $unique = $parent === null || empty($fop);
306
            $html .= '<td align="left"><table class="tv_tree"><tbody>';
307
308
            if ($parent instanceof Individual) {
309
                $u = $unique ? 'c' : 't';
310
                $html .= '<tr><td ' . ($gen == 0 ? ' abbr="p' . $primaryChildFamily->xref() . '@' . $u . '"' : '') . '>';
311
                $html .= $this->drawPerson($parent, $gen - 1, 1, $primaryChildFamily, $u, false);
312
                $html .= '</td></tr>';
313
            }
314
315
            if (count($fop)) {
316
                $n  = 0;
317
                $nb = count($fop);
318
                foreach ($fop as $p) {
319
                    $n++;
320
                    $u = $unique ? 'c' : ($n == $nb || empty($p[1]) ? 'b' : 'h');
321
                    $html .= '<tr><td ' . ($gen == 0 ? ' abbr="p' . $p[1]->xref() . '@' . $u . '"' : '') . '>' . $this->drawPerson($p[0], $gen - 1, 1, $p[1], $u, false) . '</td></tr>';
322
                }
323
            }
324
            $html .= '</tbody></table></td>';
325
        }
326
327
        if ($state < 0) {
328
            $html .= $this->drawVerticalLine($line);
329
        }
330
331
        $html .= '</tr></tbody></table>';
332
333
        if ($isRoot) {
334
            $html .= '</td><td id="tv_tree_right"></td></tr><tr><td id="tv_tree_bottomleft"></td><td id="tv_tree_bottom"></td><td id="tv_tree_bottomright"></td></tr></tbody></table>';
335
        }
336
337
        return $html;
338
    }
339
340
    /**
341
     * Draw a person name preceded by sex icon, with parents as tooltip
342
     *
343
     * @param Individual $individual The individual to draw
344
     * @param string     $dashed     Either "dashed", to print dashed top border to separate multiple spouses, or ""
345
     *
346
     * @return string
347
     */
348
    private function drawPersonName(Individual $individual, string $dashed): string
349
    {
350
        $family = $individual->childFamilies()->first();
351
        if ($family) {
352
            $family_name = strip_tags($family->fullName());
353
        } else {
354
            $family_name = I18N::translateContext('unknown family', 'unknown');
355
        }
356
        switch ($individual->sex()) {
357
            case 'M':
358
                /* I18N: e.g. “Son of [father name & mother name]” */
359
                $title = ' title="' . I18N::translate('Son of %s', $family_name) . '"';
360
                break;
361
            case 'F':
362
                /* I18N: e.g. “Daughter of [father name & mother name]” */
363
                $title = ' title="' . I18N::translate('Daughter of %s', $family_name) . '"';
364
                break;
365
            default:
366
                /* I18N: e.g. “Child of [father name & mother name]” */
367
                $title = ' title="' . I18N::translate('Child of %s', $family_name) . '"';
368
                break;
369
        }
370
        $sex = $individual->sex();
371
372
        return '<div class="tv' . $sex . ' ' . $dashed . '"' . $title . '><a href="' . e($individual->url()) . '"></a>' . $individual->fullName() . ' <span class="dates">' . $individual->lifespan() . '</span></div>';
373
    }
374
375
    /**
376
     * Get the thumbnail image for the given person
377
     *
378
     * @param Individual $individual
379
     *
380
     * @return string
381
     */
382
    private function getThumbnail(Individual $individual): string
383
    {
384
        if ($individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
385
            return $individual->displayImage(40, 50, 'crop', []);
386
        }
387
388
        return '';
389
    }
390
391
    /**
392
     * Draw a vertical line
393
     *
394
     * @param string $line A parameter that set how to draw this line with auto-resizing capabilities
395
     *
396
     * @return string
397
     * WARNING : some tricky hacks are required in CSS to ensure cross-browser compliance
398
     * some browsers shows an image, which imply a size limit in height,
399
     * and some other browsers (ex: firefox) shows a <div> tag, which have no size limit in height
400
     * Therefore, Firefox is a good choice to print very big trees.
401
     */
402
    private function drawVerticalLine(string $line): string
403
    {
404
        return '<td class="tv_vline tv_vline_' . $line . '"><div class="tv_vline tv_vline_' . $line . '"></div></td>';
405
    }
406
407
    /**
408
     * Draw an horizontal line
409
     */
410
    private function drawHorizontalLine(): string
411
    {
412
        return '<td class="tv_hline"><div class="tv_hline"></div></td>';
413
    }
414
}
415