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