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