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