TreeView   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 161
dl 0
loc 378
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
A getPersonDetails() 0 24 5
F drawPerson() 0 102 33
A drawHorizontalLine() 0 3 1
B drawChildren() 0 39 11
A drawVerticalLine() 0 3 1
B getIndividuals() 0 36 6
A __construct() 0 3 1
A drawViewport() 0 12 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;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Family was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use Fisharebest\Webtrees\Gedcom;
24
use Fisharebest\Webtrees\I18N;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\I18N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use Fisharebest\Webtrees\Individual;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Individual was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
class TreeView
35
{
36
    // HTML element name
37
    private string $name;
38
39
    /**
40
     * Treeview Constructor
41
     *
42
     * @param string $name the name of the TreeView object’s instance
43
     */
44
    public function __construct(string $name = 'tree')
45
    {
46
        $this->name = $name;
47
    }
48
49
    /**
50
     * Draw the viewport which creates the draggable/zoomable framework
51
     * Size is set by the container, as the viewport can scale itself automatically
52
     *
53
     * @param Individual $individual  Draw the chart for this individual
54
     * @param int        $generations number of generations to draw
55
     *
56
     * @return array<string>  HTML and Javascript
57
     */
58
    public function drawViewport(Individual $individual, int $generations): array
59
    {
60
        $html = view('modules/interactive-tree/chart', [
61
            'module'     => 'tree',
62
            'name'       => $this->name,
63
            'individual' => $this->drawPerson($individual, $generations, 0, null, '', true),
64
            'tree'       => $individual->tree(),
65
        ]);
66
67
        return [
68
            $html,
69
            'var ' . $this->name . 'Handler = new TreeViewHandler("' . $this->name . '", "' . e($individual->tree()->name()) . '");',
70
        ];
71
    }
72
73
    /**
74
     * Return a JSON structure to a JSON request
75
     *
76
     * @param Tree   $tree
77
     * @param string $request list of JSON requests
78
     *
79
     * @return string
80
     */
81
    public function getIndividuals(Tree $tree, string $request): string
82
    {
83
        $json_requests = explode(';', $request);
84
        $r             = [];
85
86
        foreach ($json_requests as $json_request) {
87
            $firstLetter = substr($json_request, 0, 1);
88
            $json_request = substr($json_request, 1);
89
90
            switch ($firstLetter) {
91
                case 'c':
92
                    $families = Collection::make(explode(',', $json_request))
0 ignored issues
show
Bug introduced by
explode(',', $json_request) of type string[] is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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