TreeView   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 162
dl 0
loc 380
rs 2.88
c 0
b 0
f 0
wmc 69

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getDetails() 0 11 3
A drawPersonName() 0 25 4
B getIndividuals() 0 38 6
A getPersonDetails() 0 24 5
A __construct() 0 3 1
F drawPerson() 0 102 33
A drawViewport() 0 12 1
A drawHorizontalLine() 0 3 1
B drawChildren() 0 39 11
A drawVerticalLine() 0 3 1
A getThumbnail() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like TreeView 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 TreeView, and based on these observations, apply Extract Interface, too.

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