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