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