xref: /webtrees/app/Module/FamilyBookChartModule.php (revision 2ebb07b4bd8cf7010946ea4d75bb6160666157f6)
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\Auth;
21use Fisharebest\Webtrees\Contracts\UserInterface;
22use Fisharebest\Webtrees\Functions\FunctionsPrint;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Individual;
25use Fisharebest\Webtrees\Menu;
26use Fisharebest\Webtrees\Tree;
27use Psr\Http\Message\ResponseInterface;
28use Psr\Http\Message\ServerRequestInterface;
29use stdClass;
30
31/**
32 * Class FamilyBookChartModule
33 */
34class FamilyBookChartModule extends AbstractModule implements ModuleChartInterface
35{
36    use ModuleChartTrait;
37
38    // Defaults
39    private const DEFAULT_GENERATIONS            = '2';
40    private const DEFAULT_DESCENDANT_GENERATIONS = '5';
41    private const DEFAULT_MAXIMUM_GENERATIONS    = '9';
42
43    // Limits
44    public const MINIMUM_GENERATIONS = 2;
45    public const MAXIMUM_GENERATIONS = 10;
46
47    /** @var stdClass */
48    private $box;
49
50    /** @var bool */
51    private $show_spouse;
52
53    /** @var int */
54    private $descent;
55
56    /** @var int */
57    private $bhalfheight;
58
59    /** @var int */
60    private $generations;
61
62    /** @var int */
63    private $dgenerations;
64
65    /**
66     * How should this module be identified in the control panel, etc.?
67     *
68     * @return string
69     */
70    public function title(): string
71    {
72        /* I18N: Name of a module/chart */
73        return I18N::translate('Family book');
74    }
75
76    /**
77     * A sentence describing what this module does.
78     *
79     * @return string
80     */
81    public function description(): string
82    {
83        /* I18N: Description of the “FamilyBookChart” module */
84        return I18N::translate('A chart of an individual’s ancestors and descendants, as a family book.');
85    }
86
87    /**
88     * CSS class for the URL.
89     *
90     * @return string
91     */
92    public function chartMenuClass(): string
93    {
94        return 'menu-chart-familybook';
95    }
96
97    /**
98     * Return a menu item for this chart - for use in individual boxes.
99     *
100     * @param Individual $individual
101     *
102     * @return Menu|null
103     */
104    public function chartBoxMenu(Individual $individual): ?Menu
105    {
106        return $this->chartMenu($individual);
107    }
108
109    /**
110     * The title for a specific instance of this chart.
111     *
112     * @param Individual $individual
113     *
114     * @return string
115     */
116    public function chartTitle(Individual $individual): string
117    {
118        /* I18N: %s is an individual’s name */
119        return I18N::translate('Family book of %s', $individual->fullName());
120    }
121
122    /**
123     * A form to request the chart parameters.
124     *
125     * @param ServerRequestInterface $request
126     * @param Tree                   $tree
127     * @param UserInterface          $user
128     *
129     * @return ResponseInterface
130     */
131    public function getChartAction(ServerRequestInterface $request, Tree $tree, UserInterface $user): ResponseInterface
132    {
133        $ajax       = (bool) $request->get('ajax');
134        $xref       = $request->get('xref', '');
135        $individual = Individual::getInstance($xref, $tree);
136
137        Auth::checkIndividualAccess($individual);
138        Auth::checkComponentAccess($this, 'chart', $tree, $user);
139
140        $show_spouse = (bool) $request->get('show_spouse');
141        $generations = (int) $request->get('generations', self::DEFAULT_GENERATIONS);
142        $generations = min($generations, self::MAXIMUM_GENERATIONS);
143        $generations = max($generations, self::MINIMUM_GENERATIONS);
144
145        // Generations of ancestors/descendants in each mini-tree.
146        $book_size = (int) $request->get('book_size', 2);
147        $book_size = min($book_size, 5);
148        $book_size = max($book_size, 2);
149
150        if ($ajax) {
151            return $this->chart($individual, $generations, $book_size, $show_spouse);
152        }
153
154        $ajax_url = $this->chartUrl($individual, [
155            'ajax'        => true,
156            'book_size'   => $book_size,
157            'generations' => $generations,
158            'show_spouse' => $show_spouse,
159        ]);
160
161        return $this->viewResponse('modules/family-book-chart/page', [
162            'ajax_url'            => $ajax_url,
163            'book_size'           => $book_size,
164            'generations'         => $generations,
165            'individual'          => $individual,
166            'maximum_generations' => self::MAXIMUM_GENERATIONS,
167            'minimum_generations' => self::MINIMUM_GENERATIONS,
168            'module_name'         => $this->name(),
169            'show_spouse'         => $show_spouse,
170            'title'               => $this->chartTitle($individual),
171        ]);
172    }
173
174    /**
175     * @param Individual $individual
176     * @param int        $generations
177     * @param int        $book_size
178     * @param bool       $show_spouse
179     *
180     * @return ResponseInterface
181     */
182    public function chart(Individual $individual, int $generations, int $book_size, bool $show_spouse): ResponseInterface
183    {
184        $this->box = (object) [
185            'width'  => app(ModuleThemeInterface::class)->parameter('chart-box-x'),
186            'height' => app(ModuleThemeInterface::class)->parameter('chart-box-y'),
187        ];
188
189        $this->show_spouse = $show_spouse;
190        $this->descent     = $generations;
191        $this->generations = $book_size;
192
193        $this->bhalfheight  = $this->box->height / 2;
194        $this->dgenerations = $this->maxDescendencyGenerations($individual, 0);
195
196        if ($this->dgenerations < 1) {
197            $this->dgenerations = 1;
198        }
199
200        // @TODO - this is just a wrapper around the old code.
201        ob_start();
202        $this->printFamilyBook($individual, $generations);
203        $html = ob_get_clean();
204
205        return response($html);
206    }
207
208    /**
209     * Prints descendency of passed in person
210     *
211     * @param int             $generation
212     * @param Individual|null $person
213     *
214     * @return float
215     */
216    private function printDescendency($generation, Individual $person = null): float
217    {
218        if ($generation > $this->dgenerations) {
219            return 0;
220        }
221
222        echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>';
223        $numkids = 0.0;
224
225        // Load children
226        $children = [];
227        if ($person instanceof Individual) {
228            // Count is position from center to left, dgenerations is number of generations
229            if ($generation < $this->dgenerations) {
230                // All children, from all partners
231                foreach ($person->spouseFamilies() as $family) {
232                    foreach ($family->children() as $child) {
233                        $children[] = $child;
234                    }
235                }
236            }
237        }
238        if ($generation < $this->dgenerations) {
239            if (!empty($children)) {
240                // real people
241                echo '<table cellspacing="0" cellpadding="0" border="0" >';
242                foreach ($children as $i => $child) {
243                    echo '<tr><td>';
244                    $kids    = $this->printDescendency($generation + 1, $child);
245                    $numkids += $kids;
246                    echo '</td>';
247                    // Print the lines
248                    if (count($children) > 1) {
249                        if ($i === 0) {
250                            // Adjust for the first column on left
251                            $h = round((($this->box->height * $kids) + 8) / 2); // Assumes border = 1 and padding = 3
252                            //  Adjust for other vertical columns
253                            if ($kids > 1) {
254                                $h = ($kids - 1) * 4 + $h;
255                            }
256                            echo '<td class="align-bottom">',
257                            '<img id="vline_', $child->xref(), '" src="', e(asset('css/images/vline.png')), '" width="3" height="', $h - 4, '"></td>';
258                        } elseif ($i === count($children) - 1) {
259                            // Adjust for the first column on left
260                            $h = round((($this->box->height * $kids) + 8) / 2);
261                            // Adjust for other vertical columns
262                            if ($kids > 1) {
263                                $h = ($kids - 1) * 4 + $h;
264                            }
265                            echo '<td class="align-top">',
266                            '<img class="bvertline" width="3" id="vline_', $child->xref(), '" src="', e(asset('css/images/vline.png')), '" height="', $h - 2, '"></td>';
267                        } else {
268                            echo '<td class="align-bottomm"style="background: url(', e(asset('css/images/vline.png')), ');">',
269                            '<img class="spacer"  width="3" src="', e(asset('css/images/spacer.png')), '"></td>';
270                        }
271                    }
272                    echo '</tr>';
273                }
274                echo '</table>';
275            } else {
276                // Hidden/empty boxes - to preserve the layout
277                echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>';
278                $numkids += $this->printDescendency($generation + 1, null);
279                echo '</td></tr></table>';
280            }
281            echo '</td>';
282            echo '<td>';
283        }
284
285        if ($numkids === 0.0) {
286            $numkids = 1;
287        }
288        echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td>';
289        if ($person instanceof Individual) {
290            echo FunctionsPrint::printPedigreePerson($person);
291            echo '</td><td>',
292            '<img class="linef1" src="', e(asset('css/images/hline.png')), '" width="8" height="3">';
293        } else {
294            echo '<div style="width:', $this->box->width + 19, 'px; height:', $this->box->height + 8, 'px;"></div>',
295            '</td><td>';
296        }
297
298        // Print the spouse
299        if ($generation === 1 && $person instanceof Individual) {
300            if ($this->show_spouse) {
301                foreach ($person->spouseFamilies() as $family) {
302                    $spouse = $family->spouse($person);
303                    echo '</td></tr><tr><td>';
304                    echo FunctionsPrint::printPedigreePerson($spouse);
305                    $numkids += 0.95;
306                    echo '</td><td>';
307                }
308            }
309        }
310        echo '</td></tr></table>';
311        echo '</td></tr>';
312        echo '</table>';
313
314        return $numkids;
315    }
316
317    /**
318     * Prints pedigree of the person passed in
319     *
320     * @param Individual $person
321     * @param int        $count
322     *
323     * @return void
324     */
325    private function printPersonPedigree($person, $count): void
326    {
327        if ($count >= $this->generations) {
328            return;
329        }
330
331        $genoffset = $this->generations; // handle pedigree n generations lines
332        //-- calculate how tall the lines should be
333        $lh = $this->bhalfheight * (2 ** ($genoffset - $count - 1));
334        //
335        //Prints empty table columns for children w/o parents up to the max generation
336        //This allows vertical line spacing to be consistent
337        if ($person->childFamilies()->isEmpty()) {
338            echo '<table cellspacing="0" cellpadding="0" border="0" >';
339            echo '<div class="wt-chart-box"></div>';
340
341            //-- recursively get the father’s family
342            $this->printPersonPedigree($person, $count + 1);
343            echo '</td><td></tr>';
344            echo '<div class="wt-chart-box"></div>';
345
346            //-- recursively get the mother’s family
347            $this->printPersonPedigree($person, $count + 1);
348            echo '</td><td></tr></table>';
349        }
350
351        // Empty box section done, now for regular pedigree
352        foreach ($person->childFamilies() as $family) {
353            echo '<table cellspacing="0" cellpadding="0" border="0" ><tr><td class="align-bottom">';
354            // Determine line height for two or more spouces
355            // And then adjust the vertical line for the root person only
356            $famcount = 0;
357            if ($this->show_spouse) {
358                // count number of spouses
359                $famcount += $person->spouseFamilies()->count();
360            }
361            $savlh = $lh; // Save current line height
362            if ($count == 1 && $genoffset <= $famcount) {
363                $linefactor = 0;
364                // genoffset of 2 needs no adjustment
365                if ($genoffset > 2) {
366                    $tblheight = $this->box->height + 8;
367                    if ($genoffset == 3) {
368                        if ($famcount == 3) {
369                            $linefactor = $tblheight / 2;
370                        } elseif ($famcount > 3) {
371                            $linefactor = $tblheight;
372                        }
373                    }
374                    if ($genoffset == 4) {
375                        if ($famcount == 4) {
376                            $linefactor = $tblheight;
377                        } elseif ($famcount > 4) {
378                            $linefactor = ($famcount - $genoffset) * ($tblheight * 1.5);
379                        }
380                    }
381                    if ($genoffset == 5) {
382                        if ($famcount == 5) {
383                            $linefactor = 0;
384                        } elseif ($famcount > 5) {
385                            $linefactor = $tblheight * ($famcount - $genoffset);
386                        }
387                    }
388                }
389                $lh = (($famcount - 1) * $this->box->height - $linefactor);
390                if ($genoffset > 5) {
391                    $lh = $savlh;
392                }
393            }
394            echo '<img class="line3 pvline"  src="', e(asset('css/images/vline.png')), '" width="3" height="', $lh, '"></td>',
395            '<td>',
396            '<img class="linef2" src="', e(asset('css/images/hline.png')), '" height="3"></td>',
397            '<td>';
398            $lh = $savlh; // restore original line height
399            //-- print the father box
400            echo FunctionsPrint::printPedigreePerson($family->husband());
401            echo '</td>';
402            if ($family->husband()) {
403                echo '<td>';
404                //-- recursively get the father’s family
405                $this->printPersonPedigree($family->husband(), $count + 1);
406                echo '</td>';
407            } else {
408                echo '<td>';
409                if ($genoffset > $count) {
410                    echo '<table cellspacing="0" cellpadding="0" border="0" >';
411                    for ($i = 1; $i < ((2 ** ($genoffset - $count)) / 2); $i++) {
412                        echo '<div class="wt-chart-box"></div>';
413                        echo '</tr>';
414                    }
415                    echo '</table>';
416                }
417            }
418            echo '</tr><tr>',
419            '<td class="align-top"><img class="pvline" alt="" role="presentation" src="', e(asset('css/images/vline.png')), '" width="3" height="', $lh, '"></td>',
420            '<td><img class="linef3" alt="" role="presentation" src="', e(asset('css/images/hline.png')), '" height="3"></td>',
421            '<td>';
422            //-- print the mother box
423            echo FunctionsPrint::printPedigreePerson($family->wife());
424            echo '</td>';
425            if ($family->wife()) {
426                echo '<td>';
427                //-- recursively print the mother’s family
428                $this->printPersonPedigree($family->wife(), $count + 1);
429                echo '</td>';
430            } else {
431                echo '<td>';
432                if ($count < $genoffset - 1) {
433                    echo '<table cellspacing="0" cellpadding="0" border="0" >';
434                    for ($i = 1; $i < ((2 ** (($genoffset - 1) - $count)) / 2) + 1; $i++) {
435                        echo '<div class="wt-chart-box"></div>';
436                        echo '</tr>';
437                        echo '<div class="wt-chart-box"></div>';
438                        echo '</tr>';
439                    }
440                    echo '</table>';
441                }
442            }
443            echo '</tr>',
444            '</table>';
445            break;
446        }
447    }
448
449    /**
450     * Calculates number of generations a person has
451     *
452     * @param Individual $individual
453     * @param int        $depth
454     *
455     * @return int
456     */
457    private function maxDescendencyGenerations(Individual $individual, $depth): int
458    {
459        if ($depth > $this->generations) {
460            return $depth;
461        }
462        $maxdc = $depth;
463        foreach ($individual->spouseFamilies() as $family) {
464            foreach ($family->children() as $child) {
465                $dc = $this->maxDescendencyGenerations($child, $depth + 1);
466                if ($dc >= $this->generations) {
467                    return $dc;
468                }
469                if ($dc > $maxdc) {
470                    $maxdc = $dc;
471                }
472            }
473        }
474        $maxdc++;
475        if ($maxdc == 1) {
476            $maxdc++;
477        }
478
479        return $maxdc;
480    }
481
482    /**
483     * Print a “Family Book” for an individual
484     *
485     * @param Individual $person
486     * @param int        $descent_steps
487     *
488     * @return void
489     */
490    private function printFamilyBook(Individual $person, $descent_steps): void
491    {
492        if ($descent_steps == 0) {
493            return;
494        }
495
496        echo
497        '<h3>',
498            /* I18N: %s is an individual’s name */
499        I18N::translate('Family of %s', $person->fullName()),
500        '</h3>',
501        '<table cellspacing="0" cellpadding="0" border="0" ><tr><td class="align-middle">';
502        $this->dgenerations = $this->generations;
503        $this->printDescendency(1, $person);
504        echo '</td><td class="align-middle">';
505        $this->printPersonPedigree($person, 1);
506        echo '</td></tr></table><br><br><hr class="wt-family-break"><br><br>';
507        foreach ($person->spouseFamilies() as $family) {
508            foreach ($family->children() as $child) {
509                $this->printFamilyBook($child, $descent_steps - 1);
510            }
511        }
512    }
513}
514