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