xref: /webtrees/app/Module/DescendancyChartModule.php (revision 4947e7089adb08f24bf1eef67994a106a8df60aa)
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\Family;
22use Fisharebest\Webtrees\FontAwesome;
23use Fisharebest\Webtrees\Functions\FunctionsCharts;
24use Fisharebest\Webtrees\Functions\FunctionsPrint;
25use Fisharebest\Webtrees\Gedcom;
26use Fisharebest\Webtrees\GedcomTag;
27use Fisharebest\Webtrees\I18N;
28use Fisharebest\Webtrees\Individual;
29use Fisharebest\Webtrees\Menu;
30use Fisharebest\Webtrees\Services\ChartService;
31use Fisharebest\Webtrees\Theme;
32use Fisharebest\Webtrees\Tree;
33use Fisharebest\Webtrees\User;
34use Illuminate\Support\Collection;
35use Ramsey\Uuid\Uuid;
36use Symfony\Component\HttpFoundation\Request;
37use Symfony\Component\HttpFoundation\Response;
38
39/**
40 * Class DescendancyChartModule
41 */
42class DescendancyChartModule extends AbstractModule implements ModuleChartInterface
43{
44    use ModuleChartTrait;
45
46    // Chart styles
47    public const CHART_STYLE_LIST        = 0;
48    public const CHART_STYLE_BOOKLET     = 1;
49    public const CHART_STYLE_INDIVIDUALS = 2;
50    public const CHART_STYLE_FAMILIES    = 3;
51
52    // Defaults
53    public const DEFAULT_STYLE               = self::CHART_STYLE_LIST;
54    public const DEFAULT_GENERATIONS         = '3';
55    public const DEFAULT_MAXIMUM_GENERATIONS = '9';
56
57    /** @var int[] */
58    protected $dabo_num = [];
59
60    /** @var string[] */
61    protected $dabo_sex = [];
62
63    /**
64     * How should this module be labelled on tabs, menus, etc.?
65     *
66     * @return string
67     */
68    public function title(): string
69    {
70        /* I18N: Name of a module/chart */
71        return I18N::translate('Descendants');
72    }
73
74    /**
75     * A sentence describing what this module does.
76     *
77     * @return string
78     */
79    public function description(): string
80    {
81        /* I18N: Description of the “DescendancyChart” module */
82        return I18N::translate('A chart of an individual’s descendants.');
83    }
84
85    /**
86     * CSS class for the URL.
87     *
88     * @return string
89     */
90    public function chartMenuClass(): string
91    {
92        return 'menu-chart-descendants';
93    }
94
95    /**
96     * Return a menu item for this chart - for use in individual boxes.
97     *
98     * @param Individual $individual
99     *
100     * @return Menu|null
101     */
102    public function chartBoxMenu(Individual $individual): ?Menu
103    {
104        return $this->chartMenu($individual);
105    }
106
107    /**
108     * The title for a specific instance of this chart.
109     *
110     * @param Individual $individual
111     *
112     * @return string
113     */
114    public function chartTitle(Individual $individual): string
115    {
116        /* I18N: %s is an individual’s name */
117        return I18N::translate('Descendants of %s', $individual->getFullName());
118    }
119
120    /**
121     * A form to request the chart parameters.
122     *
123     * @param Request      $request
124     * @param Tree         $tree
125     * @param User         $user
126     * @param ChartService $chart_service
127     *
128     * @return Response
129     */
130    public function getChartAction(Request $request, Tree $tree, User $user, ChartService $chart_service): Response
131    {
132        $ajax       = (bool) $request->get('ajax');
133        $xref       = $request->get('xref', '');
134        $individual = Individual::getInstance($xref, $tree);
135
136        Auth::checkIndividualAccess($individual);
137        Auth::checkComponentAccess($this, 'chart', $tree, $user);
138
139        $minimum_generations = 2;
140        $maximum_generations = (int) $tree->getPreference('MAX_DESCENDANCY_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS);
141        $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS);
142
143        $chart_style = (int) $request->get('chart_style', self::DEFAULT_STYLE);
144        $generations = (int) $request->get('generations', $default_generations);
145
146        $generations = min($generations, $maximum_generations);
147        $generations = max($generations, $minimum_generations);
148
149        if ($ajax) {
150            return $this->chart($request, $tree, $chart_service);
151        }
152
153        $ajax_url = $this->chartUrl($individual, [
154            'chart_style' => $chart_style,
155            'generations' => $generations,
156            'ajax'        => true,
157        ]);
158
159        return $this->viewResponse('modules/descendancy_chart/page', [
160            'ajax_url'            => $ajax_url,
161            'chart_style'         => $chart_style,
162            'chart_styles'        => $this->chartStyles(),
163            'default_generations' => $default_generations,
164            'generations'         => $generations,
165            'individual'          => $individual,
166            'maximum_generations' => $maximum_generations,
167            'minimum_generations' => $minimum_generations,
168            'module_name'         => $this->name(),
169            'title'               => $this->chartTitle($individual),
170        ]);
171    }
172
173    /**
174     * @param Request      $request
175     * @param Tree         $tree
176     * @param ChartService $chart_service
177     *
178     * @return Response
179     */
180    public function chart(Request $request, Tree $tree, ChartService $chart_service): Response
181    {
182        $this->layout = 'layouts/ajax';
183
184        $xref       = $request->get('xref', '');
185        $individual = Individual::getInstance($xref, $tree);
186
187        Auth::checkIndividualAccess($individual);
188
189        $minimum_generations = 2;
190        $maximum_generations = (int) $tree->getPreference('MAX_PEDIGREE_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS);
191        $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS);
192
193        $chart_style = (int) $request->get('chart_style', self::DEFAULT_STYLE);
194        $generations = (int) $request->get('generations', $default_generations);
195
196        $generations = min($generations, $maximum_generations);
197        $generations = max($generations, $minimum_generations);
198
199        switch ($chart_style) {
200            case self::CHART_STYLE_LIST:
201            default:
202                return $this->descendantsList($individual, $generations);
203
204            case self::CHART_STYLE_BOOKLET:
205                return $this->descendantsBooklet($individual, $generations);
206
207            case self::CHART_STYLE_INDIVIDUALS:
208                $individuals = $chart_service->descendants($individual, $generations - 1);
209
210                return $this->descendantsIndividuals($tree, $individuals);
211
212            case self::CHART_STYLE_FAMILIES:
213                $families = $chart_service->descendantFamilies($individual, $generations - 1);
214
215                return $this->descendantsFamilies($tree, $families);
216        }
217    }
218
219    /**
220     * Show a hierarchical list of descendants
221     *
222     * @TODO replace ob_start() with views.
223     *
224     * @param Individual $individual
225     * @param int        $generations
226     *
227     * @return Response
228     */
229    private function descendantsList(Individual $individual, int $generations): Response
230    {
231        ob_start();
232
233        echo '<ul class="chart_common">';
234        $this->printChildDescendancy($individual, $generations, $generations);
235        echo '</ul>';
236
237        $html = ob_get_clean();
238
239        return new Response($html);
240    }
241
242    /**
243     * print a child descendancy
244     *
245     * @param Individual $person
246     * @param int        $depth the descendancy depth to show
247     * @param int        $generations
248     *
249     * @return void
250     */
251    private function printChildDescendancy(Individual $person, $depth, int $generations)
252    {
253        echo '<li>';
254        echo '<table><tr><td>';
255        if ($depth == $generations) {
256            echo '<img alt="" role="presentation" src="' . Theme::theme()->parameter('image-spacer') . '" height="3" width="15"></td><td>';
257        } else {
258            echo '<img src="' . Theme::theme()->parameter('image-spacer') . '" height="3" width="3">';
259            echo '<img src="' . Theme::theme()->parameter('image-hline') . '" height="3" width="', 12, '"></td><td>';
260        }
261        echo FunctionsPrint::printPedigreePerson($person);
262        echo '</td>';
263
264        // check if child has parents and add an arrow
265        echo '<td></td>';
266        echo '<td>';
267        foreach ($person->getChildFamilies() as $cfamily) {
268            foreach ($cfamily->getSpouses() as $parent) {
269                echo FontAwesome::linkIcon('arrow-up', I18N::translate('Start at parents'), ['href' => route('descendants', ['ged'         => $parent->tree()->name(),
270                                                                                                                             'xref'        => $parent->xref(),
271                                                                                                                             'generations' => $generations,
272                ]),
273                ]);
274                // only show the arrow for one of the parents
275                break;
276            }
277        }
278
279        // d'Aboville child number
280        $level = $generations - $depth;
281        echo '<br><br>&nbsp;';
282        echo '<span dir="ltr">'; //needed so that RTL languages will display this properly
283        if (!isset($this->dabo_num[$level])) {
284            $this->dabo_num[$level] = 0;
285        }
286        $this->dabo_num[$level]++;
287        $this->dabo_num[$level + 1] = 0;
288        $this->dabo_sex[$level]     = $person->getSex();
289        for ($i = 0; $i <= $level; $i++) {
290            $isf = $this->dabo_sex[$i];
291            if ($isf === 'M') {
292                $isf = '';
293            }
294            if ($isf === 'U') {
295                $isf = 'NN';
296            }
297            echo '<span class="person_box' . $isf . '">&nbsp;' . $this->dabo_num[$i] . '&nbsp;</span>';
298            if ($i < $level) {
299                echo '.';
300            }
301        }
302        echo '</span>';
303        echo '</td></tr>';
304        echo '</table>';
305        echo '</li>';
306
307        // loop for each spouse
308        foreach ($person->getSpouseFamilies() as $family) {
309            $this->printFamilyDescendancy($person, $family, $depth, $generations);
310        }
311    }
312
313    /**
314     * print a family descendancy
315     *
316     * @param Individual $person
317     * @param Family     $family
318     * @param int        $depth the descendancy depth to show
319     * @param int        $generations
320     *
321     * @return void
322     */
323    private function printFamilyDescendancy(Individual $person, Family $family, int $depth, int $generations)
324    {
325        $uid = Uuid::uuid4()->toString(); // create a unique ID
326        // print marriage info
327        echo '<li>';
328        echo '<img src="', Theme::theme()->parameter('image-spacer'), '" height="2" width="', 19, '">';
329        echo '<span class="details1">';
330        echo '<a href="#" onclick="expand_layer(\'' . $uid . '\'); return false;" class="top"><i id="' . $uid . '_img" class="icon-minus" title="' . I18N::translate('View this family') . '"></i></a>';
331        if ($family->canShow()) {
332            foreach ($family->facts(Gedcom::MARRIAGE_EVENTS) as $fact) {
333                echo ' <a href="', e($family->url()), '" class="details1">', $fact->summary(), '</a>';
334            }
335        }
336        echo '</span>';
337
338        // print spouse
339        $spouse = $family->getSpouse($person);
340        echo '<ul class="generations" id="' . $uid . '">';
341        echo '<li>';
342        echo '<table><tr><td>';
343        echo FunctionsPrint::printPedigreePerson($spouse);
344        echo '</td>';
345
346        // check if spouse has parents and add an arrow
347        echo '<td></td>';
348        echo '<td>';
349        if ($spouse) {
350            foreach ($spouse->getChildFamilies() as $cfamily) {
351                foreach ($cfamily->getSpouses() as $parent) {
352                    echo FontAwesome::linkIcon('arrow-up', I18N::translate('Start at parents'), ['href' => route('descendants', ['ged'         => $parent->tree()->name(),
353                                                                                                                                 'xref'        => $parent->xref(),
354                                                                                                                                 'generations' => $generations,
355                    ]),
356                    ]);
357                    // only show the arrow for one of the parents
358                    break;
359                }
360            }
361        }
362        echo '<br><br>&nbsp;';
363        echo '</td></tr>';
364
365        // children
366        $children = $family->getChildren();
367        echo '<tr><td colspan="3" class="details1" >&nbsp;&nbsp;';
368        if (!empty($children)) {
369            echo GedcomTag::getLabel('NCHI') . ': ' . count($children);
370        } else {
371            // Distinguish between no children (NCHI 0) and no recorded
372            // children (no CHIL records)
373            if (strpos($family->gedcom(), '\n1 NCHI 0') !== false) {
374                echo GedcomTag::getLabel('NCHI') . ': ' . count($children);
375            } else {
376                echo I18N::translate('No children');
377            }
378        }
379        echo '</td></tr></table>';
380        echo '</li>';
381        if ($depth > 1) {
382            foreach ($children as $child) {
383                $this->printChildDescendancy($child, $depth - 1, $generations);
384            }
385        }
386        echo '</ul>';
387        echo '</li>';
388    }
389
390    /**
391     * Show a tabular list of individual descendants.
392     *
393     * @param Tree       $tree
394     * @param Collection $individuals
395     *
396     * @return Response
397     */
398    private function descendantsIndividuals(Tree $tree, Collection $individuals): Response
399    {
400        $this->layout = 'layouts/ajax';
401
402        return $this->viewResponse('lists/individuals-table', [
403            'individuals' => $individuals,
404            'sosa'        => false,
405            'tree'        => $tree,
406        ]);
407    }
408
409    /**
410     * Show a tabular list of individual descendants.
411     *
412     * @param Tree       $tree
413     * @param Collection $families
414     *
415     * @return Response
416     */
417    private function descendantsFamilies(Tree $tree, Collection $families): Response
418    {
419        $this->layout = 'layouts/ajax';
420
421        return $this->viewResponse('lists/families-table', [
422            'families' => $families,
423            'tree'     => $tree,
424        ]);
425    }
426
427    /**
428     * Show a booklet view of descendants
429     *
430     * @TODO replace ob_start() with views.
431     *
432     * @param Individual $individual
433     * @param int        $generations
434     *
435     * @return Response
436     */
437    private function descendantsBooklet(Individual $individual, int $generations): Response
438    {
439        ob_start();
440
441        $this->printChildFamily($individual, $generations);
442
443        $html = ob_get_clean();
444
445        return new Response($html);
446    }
447
448
449    /**
450     * Print a child family
451     *
452     * @param Individual $individual
453     * @param int        $depth     - the descendancy depth to show
454     * @param string     $daboville - d'Aboville number
455     * @param string     $gpid
456     *
457     * @return void
458     */
459    private function printChildFamily(Individual $individual, $depth, $daboville = '1.', $gpid = '')
460    {
461        if ($depth < 2) {
462            return;
463        }
464
465        $i = 1;
466
467        foreach ($individual->getSpouseFamilies() as $family) {
468            FunctionsCharts::printSosaFamily($family, '', -1, $daboville, $individual->xref(), $gpid, false);
469            foreach ($family->getChildren() as $child) {
470                $this->printChildFamily($child, $depth - 1, $daboville . ($i++) . '.', $individual->xref());
471            }
472        }
473    }
474
475    /**
476     * This chart can display its output in a number of styles
477     *
478     * @return array
479     */
480    private function chartStyles(): array
481    {
482        return [
483            self::CHART_STYLE_LIST        => I18N::translate('List'),
484            self::CHART_STYLE_BOOKLET     => I18N::translate('Booklet'),
485            self::CHART_STYLE_INDIVIDUALS => I18N::translate('Individuals'),
486            self::CHART_STYLE_FAMILIES    => I18N::translate('Families'),
487        ];
488    }
489}
490