xref: /webtrees/app/Module/HourglassChartModule.php (revision 9867b2f0bfec6864e75b2501f3e96895ff42db48)
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\FontAwesome;
22use Fisharebest\Webtrees\Functions\FunctionsPrint;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Individual;
25use Fisharebest\Webtrees\Menu;
26use Fisharebest\Webtrees\Theme;
27use Fisharebest\Webtrees\Tree;
28use Fisharebest\Webtrees\User;
29use Symfony\Component\HttpFoundation\Request;
30use Symfony\Component\HttpFoundation\Response;
31
32/**
33 * Class HourglassChartModule
34 */
35class HourglassChartModule extends AbstractModule implements ModuleChartInterface
36{
37    use ModuleChartTrait;
38
39    // Defaults
40    private const DEFAULT_GENERATIONS         = '3';
41    private const DEFAULT_MAXIMUM_GENERATIONS = '9';
42
43    // Limits
44    private const MINIMUM_GENERATIONS = 2;
45
46    /**
47     * How should this module be labelled on tabs, menus, etc.?
48     *
49     * @return string
50     */
51    public function title(): string
52    {
53        /* I18N: Name of a module/chart */
54        return I18N::translate('Hourglass chart');
55    }
56
57    /**
58     * A sentence describing what this module does.
59     *
60     * @return string
61     */
62    public function description(): string
63    {
64        /* I18N: Description of the “HourglassChart” module */
65        return I18N::translate('An hourglass chart of an individual’s ancestors and descendants.');
66    }
67
68    /**
69     * CSS class for the URL.
70     *
71     * @return string
72     */
73    public function chartMenuClass(): string
74    {
75        return 'menu-chart-hourglass';
76    }
77
78    /**
79     * Return a menu item for this chart - for use in individual boxes.
80     *
81     * @param Individual $individual
82     *
83     * @return Menu|null
84     */
85    public function chartBoxMenu(Individual $individual): ?Menu
86    {
87        return $this->chartMenu($individual);
88    }
89
90    /**
91     * A form to request the chart parameters.
92     *
93     * @param Request $request
94     * @param Tree    $tree
95     * @param User    $user
96     *
97     * @return Response
98     */
99    public function getChartAction(Request $request, Tree $tree, User $user): Response
100    {
101        $ajax       = (bool) $request->get('ajax');
102        $xref       = $request->get('xref', '');
103        $individual = Individual::getInstance($xref, $tree);
104
105        Auth::checkIndividualAccess($individual);
106        Auth::checkComponentAccess($this, 'chart', $tree, $user);
107
108        $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS);
109        $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS);
110
111        $generations = (int) $request->get('generations', $default_generations);
112
113        $generations = min($generations, $maximum_generations);
114        $generations = max($generations, self::MINIMUM_GENERATIONS);
115
116        $show_spouse = (bool) $request->get('show_spouse');
117
118        if ($ajax) {
119            return $this->chart($individual, $generations, $show_spouse);
120        }
121
122        $ajax_url = $this->chartUrl($individual, [
123            'ajax' => true,
124        ]);
125
126        return $this->viewResponse('modules/hourglass-chart/page', [
127            'ajax_url'            => $ajax_url,
128            'generations'         => $generations,
129            'individual'          => $individual,
130            'maximum_generations' => $maximum_generations,
131            'minimum_generations' => self::MINIMUM_GENERATIONS,
132            'module_name'         => $this->name(),
133            'show_spouse'         => $show_spouse,
134            'title'               => $this->chartTitle($individual),
135        ]);
136    }
137
138    /**
139     * Generate the initial generations of the chart
140     *
141     * @param Request $request
142     * @param Tree    $tree
143     *
144     * @return Response
145     */
146    protected function chart(Individual $individual, int $generations, bool $show_spouse): Response
147    {
148        ob_start();
149        $this->printDescendency($individual, 1, $generations, $show_spouse, true);
150        $descendants = ob_get_clean();
151
152        ob_start();
153        $this->printPersonPedigree($individual, 1, $generations, $show_spouse);
154        $ancestors = ob_get_clean();
155
156        return new Response(view('modules/hourglass-chart/chart', [
157            'descendants' => $descendants,
158            'ancestors'   => $ancestors,
159            'bhalfheight' => (int) (Theme::theme()->parameter('chart-box-y') / 2),
160            'module_name' => $this->name(),
161        ]));
162    }
163
164    /**
165     * @param Request $request
166     * @param Tree    $tree
167     *
168     * @return Response
169     */
170    public function postAncestorsAction(Request $request, Tree $tree): Response
171    {
172        $xref       = $request->get('xref', '');
173        $individual = Individual::getInstance($xref, $tree);
174
175        Auth::checkIndividualAccess($individual);
176
177        $show_spouse = (bool) $request->get('show_spouse');
178
179        ob_start();
180        $this->printPersonPedigree($individual, 0, 1, $show_spouse);
181        $html = ob_get_clean();
182
183        return new Response($html);
184    }
185
186    /**
187     * @param Request $request
188     * @param Tree    $tree
189     *
190     * @return Response
191     */
192    public function postDescendantsAction(Request $request, Tree $tree): Response
193    {
194        $xref       = $request->get('xref', '');
195        $individual = Individual::getInstance($xref, $tree);
196
197        $show_spouse = (bool) $request->get('show_spouse');
198
199        ob_start();
200        $this->printDescendency($individual, 1, 2, $show_spouse, false);
201        $html = ob_get_clean();
202
203        return new Response($html);
204    }
205
206    /**
207     * Prints descendency of passed in person
208     *
209     * @param Individual $individual  Show descendants of this individual
210     * @param int        $generation  The current generation number
211     * @param int        $generations Show this number of generations
212     * @param bool       $show_spouse
213     * @param bool       $show_menu
214     *
215     * @return void
216     */
217    private function printDescendency(Individual $individual, int $generation, int $generations, bool $show_spouse, bool $show_menu)
218    {
219        static $lastGenSecondFam = false;
220
221        if ($generation > $generations) {
222            return;
223        }
224        $pid         = $individual->xref();
225        $tablealign  = 'right';
226        $otablealign = 'left';
227        if (I18N::direction() === 'rtl') {
228            $tablealign  = 'left';
229            $otablealign = 'right';
230        }
231
232        //-- put a space between families on the last generation
233        if ($generation == $generations - 1) {
234            if ($lastGenSecondFam) {
235                echo '<br>';
236            }
237            $lastGenSecondFam = true;
238        }
239        echo '<table cellspacing="0" cellpadding="0" border="0" id="table_' . e($pid) . '" class="hourglassChart" style="float:' . $tablealign . '">';
240        echo '<tr>';
241        echo '<td style="text-align:' . $tablealign . '">';
242        $families = $individual->getSpouseFamilies();
243        $children = [];
244        if ($generation < $generations) {
245            // Put all of the children in a common array
246            foreach ($families as $family) {
247                foreach ($family->getChildren() as $child) {
248                    $children[] = $child;
249                }
250            }
251
252            $ct = count($children);
253            if ($ct > 0) {
254                echo '<table cellspacing="0" cellpadding="0" border="0" style="position: relative; top: auto; float: ' . $tablealign . ';">';
255                for ($i = 0; $i < $ct; $i++) {
256                    $individual2 = $children[$i];
257                    $chil        = $individual2->xref();
258                    echo '<tr>';
259                    echo '<td id="td_', e($chil), '" class="', I18N::direction(), '" style="text-align:', $otablealign, '">';
260                    $this->printDescendency($individual2, $generation + 1, $generations, $show_spouse, false);
261                    echo '</td>';
262
263                    // Print the lines
264                    if ($ct > 1) {
265                        if ($i == 0) {
266                            // First child
267                            echo '<td style="vertical-align:bottom"><img alt="" role="presentation" class="line1 tvertline" id="vline_' . $chil . '" src="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>';
268                        } elseif ($i == $ct - 1) {
269                            // Last child
270                            echo '<td style="vertical-align:top"><img alt="" role="presentation" class="bvertline" id="vline_' . $chil . '" src="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>';
271                        } else {
272                            // Middle child
273                            echo '<td style="background: url(\'' . Theme::theme()->parameter('image-vline') . '\');"><img alt="" role="presentation" src="' . Theme::theme()->parameter('image-spacer') . '" width="3"></td>';
274                        }
275                    }
276                    echo '</tr>';
277                }
278                echo '</table>';
279            }
280            echo '</td>';
281            echo '<td class="myCharts" width="', Theme::theme()->parameter('chart-box-x'), '">';
282        }
283
284        // Print the descendency expansion arrow
285        if ($generation == $generations) {
286            $tbwidth = Theme::theme()->parameter('chart-box-x') + 16;
287            for ($j = $generation; $j < $generations; $j++) {
288                echo "<div style='width: ", $tbwidth, "px;'><br></div></td><td style='width:", Theme::theme()->parameter('chart-box-x'), "px'>";
289            }
290            $kcount = 0;
291            foreach ($families as $family) {
292                $kcount += $family->getNumberOfChildren();
293            }
294            if ($kcount == 0) {
295                echo "</td><td style='width:", Theme::theme()->parameter('chart-box-x'), "px'>";
296            } else {
297                echo FontAwesome::linkIcon('arrow-start', I18N::translate('Children'), [
298                    'href'         => '#',
299                    'data-route'   => 'Descendants',
300                    'data-xref'    => $pid,
301                    'data-spouses' => $show_spouse,
302                    'data-tree'    => $individual->tree()->name(),
303                ]);
304
305                //-- move the arrow up to line up with the correct box
306                if ($show_spouse) {
307                    echo str_repeat('<br><br><br>', count($families));
308                }
309                echo "</td><td style='width:", Theme::theme()->parameter('chart-box-x'), "px'>";
310            }
311        }
312
313        echo '<table cellspacing="0" cellpadding="0" border="0" id="table2_' . $pid . '"><tr><td> ';
314        echo FunctionsPrint::printPedigreePerson($individual);
315        echo '</td><td> <img alt="" role="presentation" class="lineh1" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3">';
316
317        //----- Print the spouse
318        if ($show_spouse) {
319            foreach ($families as $family) {
320                echo "</td></tr><tr><td style='text-align:$otablealign'>";
321                echo FunctionsPrint::printPedigreePerson($family->getSpouse($individual));
322                echo '</td><td> </td>';
323            }
324            //-- add offset divs to make things line up better
325            if ($generation == $generations) {
326                echo "<tr><td colspan '2'><div style='height:", (Theme::theme()->parameter('chart-box-y') / 4), 'px; width:', Theme::theme()->parameter('chart-box-x'), "px;'><br></div>";
327            }
328        }
329        echo '</td></tr></table>';
330
331        // For the root individual, print a down arrow that allows changing the root of tree
332        if ($show_menu && $generation == 1) {
333            echo '<div class="center" id="childarrow" style="position:absolute; width:', Theme::theme()->parameter('chart-box-x'), 'px;">';
334            echo FontAwesome::linkIcon('arrow-down', I18N::translate('Family'), [
335                'href' => '#',
336                'id'   => 'spouse-child-links',
337            ]);
338            echo '<div id="childbox">';
339            echo '<table cellspacing="0" cellpadding="0" border="0" class="person_box"><tr><td> ';
340
341            foreach ($individual->getSpouseFamilies() as $family) {
342                echo "<span class='name1'>" . I18N::translate('Family') . '</span>';
343                $spouse = $family->getSpouse($individual);
344                if ($spouse !== null) {
345                    echo '<a href="' . e(route('hourglass', [
346                            'xref'        => $spouse->xref(),
347                            'generations' => $generations,
348                            'show_spouse' => (int) $show_spouse,
349                            'ged'         => $spouse->tree()->name(),
350                        ])) . '" class="name1">' . $spouse->getFullName() . '</a>';
351                }
352                foreach ($family->getChildren() as $child) {
353                    echo '<a href="' . e(route('hourglass', [
354                            'xref'        => $child->xref(),
355                            'generations' => $generations,
356                            'show_spouse' => (int) $show_spouse,
357                            'ged'         => $child->tree()->name(),
358                        ])) . '" class="name1">' . $child->getFullName() . '</a>';
359                }
360            }
361
362            //-- print the siblings
363            foreach ($individual->getChildFamilies() as $family) {
364                if ($family->getHusband() || $family->getWife()) {
365                    echo "<span class='name1'>" . I18N::translate('Parents') . '</span>';
366                    $husb = $family->getHusband();
367                    if ($husb) {
368                        echo '<a href="' . e(route('hourglass', [
369                                'xref'        => $husb->xref(),
370                                'generations' => $generations,
371                                'show_spouse' => (int) $show_spouse,
372                                'ged'         => $husb->tree()->name(),
373                            ])) . '" class="name1">' . $husb->getFullName() . '</a>';
374                    }
375                    $wife = $family->getWife();
376                    if ($wife) {
377                        echo '<a href="' . e(route('hourglass', [
378                                'xref'        => $wife->xref(),
379                                'generations' => $generations,
380                                'show_spouse' => (int) $show_spouse,
381                                'ged'         => $wife->tree()->name(),
382                            ])) . '" class="name1">' . $wife->getFullName() . '</a>';
383                    }
384                }
385
386                // filter out root person from children array so only siblings remain
387                $siblings       = array_filter($family->getChildren(), function (Individual $x) use ($individual): bool {
388                    return $x !== $individual;
389                });
390                $count_siblings = count($siblings);
391                if ($count_siblings > 0) {
392                    echo '<span class="name1">';
393                    echo $count_siblings > 1 ? I18N::translate('Siblings') : I18N::translate('Sibling');
394                    echo '</span>';
395                    foreach ($siblings as $child) {
396                        echo '<a href="' . e(route('hourglass', [
397                                'xref'        => $child->xref(),
398                                'generations' => $generations,
399                                'show_spouse' => (int) $show_spouse,
400                                'ged'         => $child->tree()->name(),
401                            ])) . '" class="name1">' . $child->getFullName() . '</a>';
402                    }
403                }
404            }
405            echo '</td></tr></table>';
406            echo '</div>';
407            echo '</div>';
408        }
409        echo '</td></tr></table>';
410    }
411
412    /**
413     * Prints pedigree of the person passed in. Which is the descendancy
414     *
415     * @param Individual $individual  Show the pedigree of this individual
416     * @param int        $generation  Current generation number
417     * @param int        $generations Show this number of generations
418     * @param bool       $show_spouse
419     *
420     * @return void
421     */
422    private function printPersonPedigree(Individual $individual, int $generation, int $generations, bool $show_spouse)
423    {
424        if ($generation >= $generations) {
425            return;
426        }
427
428        // handle pedigree n generations lines
429        $genoffset = $generations;
430
431        $family = $individual->getPrimaryChildFamily();
432
433        if ($family === null) {
434            // Prints empty table columns for children w/o parents up to the max generation
435            // This allows vertical line spacing to be consistent
436            echo '<table><tr><td> ' . Theme::theme()->individualBoxEmpty() . '</td>';
437            echo '<td> ';
438            // Recursively get the father’s family
439            $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse);
440            echo '</td></tr>';
441            echo '<tr><td> ' . Theme::theme()->individualBoxEmpty() . '</td>';
442            echo '<td> ';
443            // Recursively get the mother’s family
444            $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse);
445            echo '</td><td> </tr></table>';
446        } else {
447            echo '<table cellspacing="0" cellpadding="0" border="0"  class="hourglassChart">';
448            echo '<tr>';
449            echo '<td style="vertical-align:bottom"><img alt="" role="presnentation" class="line3 pvline" src="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>';
450            echo '<td> <img alt="" role="presentation" class="lineh2" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3"></td>';
451            echo '<td class="myCharts"> ';
452            //-- print the father box
453            echo FunctionsPrint::printPedigreePerson($family->getHusband());
454            echo '</td>';
455            if ($family->getHusband()) {
456                $ARID = $family->getHusband()->xref();
457                echo '<td id="td_' . e($ARID) . '">';
458
459                if ($generation == $generations - 1 && $family->getHusband()->getChildFamilies()) {
460                    echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [
461                        'href'         => '#',
462                        'data-route'   => 'Ancestors',
463                        'data-xref'    => $ARID,
464                        'data-spouses' => (int) $show_spouse,
465                        'data-tree'    => $family->getHusband()->tree()->name(),
466                    ]);
467                }
468
469                $this->printPersonPedigree($family->getHusband(), $generation + 1, $generations, $show_spouse);
470                echo '</td>';
471            } else {
472                echo '<td> ';
473                if ($generation < $genoffset - 1) {
474                    echo '<table>';
475                    for ($i = $generation; $i < ((2 ** (($genoffset - 1) - $generation)) / 2) + 2; $i++) {
476                        echo Theme::theme()->individualBoxEmpty();
477                        echo '</tr>';
478                        echo Theme::theme()->individualBoxEmpty();
479                        echo '</tr>';
480                    }
481                    echo '</table>';
482                }
483            }
484            echo
485            '</tr><tr>',
486                '<td style="vertical-align:top"><img alt="" role="presentation" class="pvline" src="' . Theme::theme()->parameter('image-vline') . '" width="3"></td>',
487                '<td> <img alt="" role="presentation" class="lineh3" src="' . Theme::theme()->parameter('image-hline') . '" width="7" height="3"></td>',
488            '<td class="myCharts"> ';
489
490            echo FunctionsPrint::printPedigreePerson($family->getWife());
491            echo '</td>';
492            if ($family->getWife()) {
493                $ARID = $family->getWife()->xref();
494                echo '<td id="td_' . e($ARID) . '">';
495
496                if ($generation == $generations - 1 && $family->getWife()->getChildFamilies()) {
497                    echo FontAwesome::linkIcon('arrow-end', I18N::translate('Parents'), [
498                        'href'         => '#',
499                        'data-route'   => 'Ancestors',
500                        'data-xref'    => $ARID,
501                        'data-spouses' => (int) $show_spouse,
502                        'data-tree'    => $family->getWife()->tree()->name(),
503                    ]);
504                }
505
506                $this->printPersonPedigree($family->getWife(), $generation + 1, $generations, $show_spouse);
507                echo '</td>';
508            }
509            echo '</tr></table>';
510        }
511    }
512}
513