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