xref: /webtrees/app/Module/HourglassChartModule.php (revision 54bcf2f03e928f1b6c932b446789c20df3ee26e2)
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\Contracts\UserInterface;
22use Fisharebest\Webtrees\Functions\FunctionsPrint;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Individual;
25use Fisharebest\Webtrees\Menu;
26use Fisharebest\Webtrees\Tree;
27use Symfony\Component\HttpFoundation\Request;
28use Symfony\Component\HttpFoundation\Response;
29use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
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 MAXIMUM_GENERATIONS = 10;
44    private const MINIMUM_GENERATIONS = 2;
45
46    /**
47     * How should this module be identified in the control panel, 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 UserInterface $user
96     *
97     * @return Response
98     */
99    public function getChartAction(Request $request, Tree $tree, UserInterface $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        $generations = (int) $request->get('generations', self::DEFAULT_GENERATIONS);
109
110        $generations = min($generations, self::MAXIMUM_GENERATIONS);
111        $generations = max($generations, self::MINIMUM_GENERATIONS);
112
113        $show_spouse = (bool) $request->get('show_spouse');
114
115        if ($ajax) {
116            return $this->chart($individual, $generations, $show_spouse);
117        }
118
119        $ajax_url = $this->chartUrl($individual, [
120            'ajax'        => true,
121            'generations' => $generations,
122            'show_spouse' => $show_spouse,
123        ]);
124
125        return $this->viewResponse('modules/hourglass-chart/page', [
126            'ajax_url'            => $ajax_url,
127            'generations'         => $generations,
128            'individual'          => $individual,
129            'maximum_generations' => self::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 Individual $individual
141     * @param int        $generations
142     * @param bool       $show_spouse
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) (app(ModuleThemeInterface::class)->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        $show_spouse = (bool) $request->get('show_spouse');
195        $xref       = $request->get('xref', '');
196        $individual = Individual::getInstance($xref, $tree);
197
198        if ($individual === null) {
199            throw new NotFoundHttpException();
200        }
201
202        ob_start();
203        $this->printDescendency($individual, 1, 2, $show_spouse, false);
204        $html = ob_get_clean();
205
206        return new Response($html);
207    }
208
209    /**
210     * Prints descendency of passed in person
211     *
212     * @param Individual $individual  Show descendants of this individual
213     * @param int        $generation  The current generation number
214     * @param int        $generations Show this number of generations
215     * @param bool       $show_spouse
216     * @param bool       $show_menu
217     *
218     * @return void
219     */
220    private function printDescendency(Individual $individual, int $generation, int $generations, bool $show_spouse, bool $show_menu): void
221    {
222        static $lastGenSecondFam = false;
223
224        if ($generation > $generations) {
225            return;
226        }
227        $pid         = $individual->xref();
228        $tablealign  = 'right';
229        $otablealign = 'left';
230        if (I18N::direction() === 'rtl') {
231            $tablealign  = 'left';
232            $otablealign = 'right';
233        }
234
235        //-- put a space between families on the last generation
236        if ($generation == $generations - 1) {
237            if ($lastGenSecondFam) {
238                echo '<br>';
239            }
240            $lastGenSecondFam = true;
241        }
242        echo '<table cellspacing="0" cellpadding="0" border="0" id="table_' . e($pid) . '" class="hourglassChart" style="float:' . $tablealign . '">';
243        echo '<tr>';
244        echo '<td style="text-align:' . $tablealign . '">';
245        $families = $individual->spouseFamilies();
246        $children = [];
247        if ($generation < $generations) {
248            // Put all of the children in a common array
249            foreach ($families as $family) {
250                foreach ($family->children() as $child) {
251                    $children[] = $child;
252                }
253            }
254
255            $ct = count($children);
256            if ($ct > 0) {
257                echo '<table cellspacing="0" cellpadding="0" border="0" style="position: relative; top: auto; float: ' . $tablealign . ';">';
258                for ($i = 0; $i < $ct; $i++) {
259                    $individual2 = $children[$i];
260                    $chil        = $individual2->xref();
261                    echo '<tr>';
262                    echo '<td id="td_', e($chil), '" class="', I18N::direction(), '" style="text-align:', $otablealign, '">';
263                    $this->printDescendency($individual2, $generation + 1, $generations, $show_spouse, false);
264                    echo '</td>';
265
266                    // Print the lines
267                    if ($ct > 1) {
268                        if ($i == 0) {
269                            // First child
270                            echo '<td style="vertical-align:bottom"><img alt="" role="presentation" class="line1 tvertline" id="vline_' . $chil . '" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>';
271                        } elseif ($i == $ct - 1) {
272                            // Last child
273                            echo '<td style="vertical-align:top"><img alt="" role="presentation" class="bvertline" id="vline_' . $chil . '" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>';
274                        } else {
275                            // Middle child
276                            echo '<td style="background:url(' . e('"' . asset('css/images/vline.png') . '"') . ');"><img alt="" role="presentation" src="' . e(asset('css/images/spacer.png')) . '" width="3"></td>';
277                        }
278                    }
279                    echo '</tr>';
280                }
281                echo '</table>';
282            }
283            echo '</td>';
284            echo '<td class="myCharts" width="', app(ModuleThemeInterface::class)->parameter('chart-box-x'), '">';
285        }
286
287        // Print the descendency expansion arrow
288        if ($generation === $generations) {
289            $tbwidth = app(ModuleThemeInterface::class)->parameter('chart-box-x') + 16;
290            for ($j = $generation; $j < $generations; $j++) {
291                echo "<div style='width: ", $tbwidth, "px;'><br></div></td><td style='width:", app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>";
292            }
293            $kcount = 0;
294            foreach ($families as $family) {
295                $kcount += $family->numberOfChildren();
296            }
297            if ($kcount == 0) {
298                echo "</td><td style='width:", app(ModuleThemeInterface::class)->parameter('chart-box-x'), "px'>";
299            } else {
300                echo '<a href="#" title="' . I18N::translate('Children') . '" data-route="Descendants" data-xref="' .  e($pid) . '" data-spouses="' .  e($show_spouse) . '" data-tree="' .  e($individual->tree()->name()) . '">' . view('icons/arrow-left') . '</a>';
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:", app(ModuleThemeInterface::class)->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="' . e(asset('css/images/hline.png')) . '" 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->spouse($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:", (app(ModuleThemeInterface::class)->parameter('chart-box-y') / 4), 'px; width:', app(ModuleThemeInterface::class)->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="text-center" id="childarrow" style="position:absolute; width:', app(ModuleThemeInterface::class)->parameter('chart-box-x'), 'px;">';
331            echo '<a href="#" title="' . I18N::translate('Family') . '" id="spouse-child-links">' . view('icons/arrow-down') . '</a>';
332            echo '<div id="childbox">';
333            echo '<table cellspacing="0" cellpadding="0" border="0" class="person_box"><tr><td> ';
334
335            foreach ($individual->spouseFamilies() as $family) {
336                echo "<span class='name1'>" . I18N::translate('Family') . '</span>';
337                $spouse = $family->spouse($individual);
338                if ($spouse !== null) {
339                    echo '<a href="' . e(route('hourglass', [
340                            'xref'        => $spouse->xref(),
341                            'generations' => $generations,
342                            'show_spouse' => (int) $show_spouse,
343                            'ged'         => $spouse->tree()->name(),
344                        ])) . '" class="name1">' . $spouse->fullName() . '</a>';
345                }
346                foreach ($family->children() as $child) {
347                    echo '<a href="' . e(route('hourglass', [
348                            'xref'        => $child->xref(),
349                            'generations' => $generations,
350                            'show_spouse' => (int) $show_spouse,
351                            'ged'         => $child->tree()->name(),
352                        ])) . '" class="name1">' . $child->fullName() . '</a>';
353                }
354            }
355
356            //-- print the siblings
357            foreach ($individual->childFamilies() as $family) {
358                if ($family->husband() || $family->wife()) {
359                    echo "<span class='name1'>" . I18N::translate('Parents') . '</span>';
360                    $husb = $family->husband();
361                    if ($husb) {
362                        echo '<a href="' . e(route('hourglass', [
363                                'xref'        => $husb->xref(),
364                                'generations' => $generations,
365                                'show_spouse' => (int) $show_spouse,
366                                'ged'         => $husb->tree()->name(),
367                            ])) . '" class="name1">' . $husb->fullName() . '</a>';
368                    }
369                    $wife = $family->wife();
370                    if ($wife) {
371                        echo '<a href="' . e(route('hourglass', [
372                                'xref'        => $wife->xref(),
373                                'generations' => $generations,
374                                'show_spouse' => (int) $show_spouse,
375                                'ged'         => $wife->tree()->name(),
376                            ])) . '" class="name1">' . $wife->fullName() . '</a>';
377                    }
378                }
379
380                // filter out root person from children array so only siblings remain
381                $siblings       = $family->children()->filter(static function (Individual $x) use ($individual): bool {
382                    return $x !== $individual;
383                });
384
385                if ($siblings->count() > 0) {
386                    echo '<span class="name1">';
387                    echo $siblings->count() > 1 ? I18N::translate('Siblings') : I18N::translate('Sibling');
388                    echo '</span>';
389                    foreach ($siblings as $child) {
390                        echo '<a href="' . e(route('hourglass', [
391                                'xref'        => $child->xref(),
392                                'generations' => $generations,
393                                'show_spouse' => (int) $show_spouse,
394                                'ged'         => $child->tree()->name(),
395                            ])) . '" class="name1">' . $child->fullName() . '</a>';
396                    }
397                }
398            }
399            echo '</td></tr></table>';
400            echo '</div>';
401            echo '</div>';
402        }
403        echo '</td></tr></table>';
404    }
405
406    /**
407     * Prints pedigree of the person passed in. Which is the descendancy
408     *
409     * @param Individual $individual  Show the pedigree of this individual
410     * @param int        $generation  Current generation number
411     * @param int        $generations Show this number of generations
412     * @param bool       $show_spouse
413     *
414     * @return void
415     */
416    private function printPersonPedigree(Individual $individual, int $generation, int $generations, bool $show_spouse): void
417    {
418        if ($generation >= $generations) {
419            return;
420        }
421
422        // handle pedigree n generations lines
423        $genoffset = $generations;
424
425        $family = $individual->primaryChildFamily();
426
427        if ($family === null) {
428            // Prints empty table columns for children w/o parents up to the max generation
429            // This allows vertical line spacing to be consistent
430            echo '<table><tr><td><div class="wt-chart-box"></div></td>';
431            echo '<td> ';
432            // Recursively get the father’s family
433            $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse);
434            echo '</td></tr>';
435            echo '<tr><td><div class="wt-chart-box"></div></td>';
436            echo '<td> ';
437            // Recursively get the mother’s family
438            $this->printPersonPedigree($individual, $generation + 1, $generations, $show_spouse);
439            echo '</td><td> </tr></table>';
440        } else {
441            echo '<table cellspacing="0" cellpadding="0" border="0"  class="hourglassChart">';
442            echo '<tr>';
443            echo '<td style="vertical-align:bottom"><img alt="" role="presnentation" class="line3 pvline" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>';
444            echo '<td> <img alt="" role="presentation" class="lineh2" src="' . e(asset('css/images/hline.png')) . '" width="7" height="3"></td>';
445            echo '<td class="myCharts"> ';
446            //-- print the father box
447            echo FunctionsPrint::printPedigreePerson($family->husband());
448            echo '</td>';
449            if ($family->husband()) {
450                $ARID = $family->husband()->xref();
451                echo '<td id="td_' . e($ARID) . '">';
452
453                if ($generation == $generations - 1 && $family->husband()->childFamilies()) {
454                    echo '<a href="#" title="' . I18N::translate('Parents') . '" data-route="Ancestors" data-xref="' .  e($ARID) . '" data-spouses="' .  e($show_spouse) . '" data-tree="' .  e($family->husband()->tree()->name()) . '">' . view('icons/arrow-right') . '</a>';
455                }
456
457                $this->printPersonPedigree($family->husband(), $generation + 1, $generations, $show_spouse);
458                echo '</td>';
459            } else {
460                echo '<td> ';
461                if ($generation < $genoffset - 1) {
462                    echo '<table>';
463                    for ($i = $generation; $i < ((2 ** (($genoffset - 1) - $generation)) / 2) + 2; $i++) {
464                        echo '<div class="wt-chart-box"></div>';
465                        echo '</tr>';
466                        echo '<div class="wt-chart-box"></div>';
467                        echo '</tr>';
468                    }
469                    echo '</table>';
470                }
471            }
472            echo
473            '</tr><tr>',
474                '<td style="vertical-align:top"><img alt="" role="presentation" class="pvline" src="' . e(asset('css/images/vline.png')) . '" width="3"></td>',
475                '<td> <img alt="" role="presentation" class="lineh3" src="' . e(asset('css/images/hline.png')) . '" width="7" height="3"></td>',
476            '<td class="myCharts"> ';
477
478            echo FunctionsPrint::printPedigreePerson($family->wife());
479            echo '</td>';
480            if ($family->wife()) {
481                $ARID = $family->wife()->xref();
482                echo '<td id="td_' . e($ARID) . '">';
483
484                if ($generation == $generations - 1 && $family->wife()->childFamilies()) {
485                    echo '<a href="#" title="' . I18N::translate('Parents') . '" data-route="Ancestors" data-xref="' .  e($ARID) . '" data-spouses="' .  e($show_spouse) . '" data-tree="' .  e($family->wife()->tree()->name()) . '">' . view('icons/arrow-right') . '</a>';
486                }
487
488                $this->printPersonPedigree($family->wife(), $generation + 1, $generations, $show_spouse);
489                echo '</td>';
490            }
491            echo '</tr></table>';
492        }
493    }
494}
495