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