xref: /webtrees/app/Module/AncestorsChartModule.php (revision 3c04ac885329c9488ff9db0a765e872bf41b9781)
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\FontAwesome;
22use Fisharebest\Webtrees\Functions\FunctionsCharts;
23use Fisharebest\Webtrees\Functions\FunctionsPrint;
24use Fisharebest\Webtrees\Gedcom;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Individual;
27use Fisharebest\Webtrees\Menu;
28use Fisharebest\Webtrees\Services\ChartService;
29use Fisharebest\Webtrees\Tree;
30use Fisharebest\Webtrees\User;
31use Illuminate\Support\Collection;
32use Symfony\Component\HttpFoundation\Request;
33use Symfony\Component\HttpFoundation\Response;
34use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
35
36/**
37 * Class AncestorsChartModule
38 */
39class AncestorsChartModule extends AbstractModule implements ModuleChartInterface
40{
41    use ModuleChartTrait;
42
43    // Chart styles
44    protected const CHART_STYLE_LIST        = 'list';
45    protected const CHART_STYLE_BOOKLET     = 'booklet';
46    protected const CHART_STYLE_INDIVIDUALS = 'individuals';
47    protected const CHART_STYLE_FAMILIES    = 'families';
48
49    // Defaults
50    protected const DEFAULT_COUSINS             = false;
51    protected const DEFAULT_STYLE               = self::CHART_STYLE_LIST;
52    protected const DEFAULT_GENERATIONS         = '3';
53    protected const DEFAULT_MAXIMUM_GENERATIONS = '9';
54
55    /**
56     * How should this module be labelled on tabs, menus, etc.?
57     *
58     * @return string
59     */
60    public function title(): string
61    {
62        /* I18N: Name of a module/chart */
63        return I18N::translate('Ancestors');
64    }
65
66    /**
67     * A sentence describing what this module does.
68     *
69     * @return string
70     */
71    public function description(): string
72    {
73        /* I18N: Description of the “AncestorsChart” module */
74        return I18N::translate('A chart of an individual’s ancestors.');
75    }
76
77    /**
78     * CSS class for the URL.
79     *
80     * @return string
81     */
82    public function chartMenuClass(): string
83    {
84        return 'menu-chart-ancestry';
85    }
86
87    /**
88     * Return a menu item for this chart - for use in individual boxes.
89     *
90     * @param Individual $individual
91     *
92     * @return Menu|null
93     */
94    public function chartBoxMenu(Individual $individual): ?Menu
95    {
96        return $this->chartMenu($individual);
97    }
98
99    /**
100     * The title for a specific instance of this chart.
101     *
102     * @param Individual $individual
103     *
104     * @return string
105     */
106    public function chartTitle(Individual $individual): string
107    {
108        /* I18N: %s is an individual’s name */
109        return I18N::translate('Ancestors of %s', $individual->getFullName());
110    }
111
112    /**
113     * A form to request the chart parameters.
114     *
115     * @param Request      $request
116     * @param Tree         $tree
117     * @param User         $user
118     * @param ChartService $chart_service
119     *
120     * @return Response
121     */
122    public function getChartAction(Request $request, Tree $tree, User $user, ChartService $chart_service): Response
123    {
124        $ajax       = (bool) $request->get('ajax');
125        $xref       = $request->get('xref', '');
126        $individual = Individual::getInstance($xref, $tree);
127
128        Auth::checkIndividualAccess($individual);
129        Auth::checkComponentAccess($this, 'chart', $tree, $user);
130
131        $minimum_generations = 2;
132        $maximum_generations = (int) $tree->getPreference('MAX_PEDIGREE_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS);
133        $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS);
134
135        $show_cousins = (bool) $request->get('show_cousins', self::DEFAULT_COUSINS);
136        $chart_style  = $request->get('chart_style', self::DEFAULT_STYLE);
137        $generations  = (int) $request->get('generations', $default_generations);
138
139        $generations = min($generations, $maximum_generations);
140        $generations = max($generations, $minimum_generations);
141
142        if ($ajax) {
143            $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations);
144
145            switch ($chart_style) {
146                default:
147                case self::CHART_STYLE_LIST:
148                    return $this->ancestorsList($individual, $generations);
149
150                case self::CHART_STYLE_BOOKLET:
151                    return $this->ancestorsBooklet($ancestors, $show_cousins);
152
153                case self::CHART_STYLE_INDIVIDUALS:
154                    return $this->ancestorsIndividuals($tree, $ancestors);
155
156                case self::CHART_STYLE_FAMILIES:
157                    return $this->ancestorsFamilies($tree, $ancestors);
158            }
159        }
160
161        $ajax_url = $this->chartUrl($individual, [
162            'generations'  => $generations,
163            'chart_style'  => $chart_style,
164            'show_cousins' => $show_cousins,
165            'ajax'         => true,
166        ]);
167
168        return $this->viewResponse('modules/ancestors-chart/page', [
169            'ajax_url'            => $ajax_url,
170            'chart_style'         => $chart_style,
171            'chart_styles'        => $this->chartStyles(),
172            'default_generations' => $default_generations,
173            'generations'         => $generations,
174            'individual'          => $individual,
175            'maximum_generations' => $maximum_generations,
176            'minimum_generations' => $minimum_generations,
177            'module_name'         => $this->name(),
178            'show_cousins'        => $show_cousins,
179            'title'               => $this->chartTitle($individual),
180        ]);
181    }
182
183    /**
184     * Show a hierarchical list of ancestors
185     *
186     * @TODO replace ob_start() with views.
187     *
188     * @param Individual $individual
189     * @param int        $generations
190     *
191     * @return Response
192     */
193    protected function ancestorsList(Individual $individual, int $generations): Response
194    {
195        ob_start();
196
197        $this->printChildAscendancy($individual, 1, $generations - 1);
198
199        $html = ob_get_clean();
200
201        $html = '<ul class="chart_common">' . $html . '</ul>';
202
203        return new Response($html);
204    }
205
206    /**
207     * print a child ascendancy
208     *
209     * @param Individual $individual
210     * @param int        $sosa
211     * @param int        $generations
212     *
213     * @return void
214     */
215    protected function printChildAscendancy(Individual $individual, $sosa, $generations)
216    {
217        echo '<li class="wt-ancestors-chart-list-item">';
218        echo '<table><tbody><tr><td>';
219        if ($sosa === 1) {
220            echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="3" width="15"></td><td>';
221        } else {
222            echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="3" width="2">';
223            echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-hline'), '" height="3" width="13"></td><td>';
224        }
225        echo FunctionsPrint::printPedigreePerson($individual);
226        echo '</td><td>';
227        if ($sosa > 1) {
228            echo FontAwesome::linkIcon('arrow-down', $this->chartTitle($individual), [
229                'href' => $this->chartUrl($individual, [
230                    'generations' => $generations,
231                    'chart_style' => self::CHART_STYLE_LIST,
232                ])
233            ]);
234        }
235        echo '</td><td class="details1">&nbsp;<span class="person_box' . ($sosa === 1 ? 'NN' : ($sosa % 2 ? 'F' : '')) . '">', I18N::number($sosa), '</span> ';
236        echo '</td><td class="details1">&nbsp;', FunctionsCharts::getSosaName($sosa), '</td>';
237        echo '</tr></tbody></table>';
238
239        // Parents
240        $family = $individual->getPrimaryChildFamily();
241        if ($family && $generations > 0) {
242            // Marriage details
243            echo '<span class="details1">';
244            echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="2" width="15"><a href="#" onclick="return expand_layer(\'sosa_', $sosa, '\');" class="top"><i id="sosa_', $sosa, '_img" class="icon-minus" title="', I18N::translate('View this family'), '"></i></a>';
245            echo ' <span class="person_box">', I18N::number($sosa * 2), '</span> ', I18N::translate('and');
246            echo ' <span class="person_boxF">', I18N::number($sosa * 2 + 1), '</span>';
247            if ($family->canShow()) {
248                foreach ($family->facts(Gedcom::MARRIAGE_EVENTS) as $fact) {
249                    echo ' <a href="', e($family->url()), '" class="details1">', $fact->summary(), '</a>';
250                }
251            }
252            echo '</span>';
253            echo '<ul class="wt-ancestors-chart-list" id="sosa_', $sosa, '">';
254            if ($family->getHusband()) {
255                $this->printChildAscendancy($family->getHusband(), $sosa * 2, $generations - 1);
256            }
257            if ($family->getWife()) {
258                $this->printChildAscendancy($family->getWife(), $sosa * 2 + 1, $generations - 1);
259            }
260            echo '</ul>';
261        }
262        echo '</li>';
263    }
264
265    /**
266     * Show a tabular list of individual ancestors.
267     *
268     * @param Tree       $tree
269     * @param Collection $ancestors
270     *
271     * @return Response
272     */
273    protected function ancestorsIndividuals(Tree $tree, Collection $ancestors): Response
274    {
275        $this->layout = 'layouts/ajax';
276
277        return $this->viewResponse('lists/individuals-table', [
278            'individuals' => $ancestors,
279            'sosa'        => true,
280            'tree'        => $tree,
281        ]);
282    }
283
284    /**
285     * Show a tabular list of individual ancestors.
286     *
287     * @param Tree       $tree
288     * @param Collection $ancestors
289     *
290     * @return Response
291     */
292    protected function ancestorsFamilies(Tree $tree, Collection $ancestors): Response
293    {
294        $this->layout = 'layouts/ajax';
295
296        $families = [];
297        foreach ($ancestors as $individual) {
298            foreach ($individual->getChildFamilies() as $family) {
299                $families[$family->xref()] = $family;
300            }
301        }
302
303        return $this->viewResponse('lists/families-table', [
304            'families' => $families,
305            'tree'     => $tree,
306        ]);
307    }
308
309    /**
310     * Show a booklet view of ancestors
311     *
312     * @TODO replace ob_start() with views.
313     *
314     * @param Collection $ancestors
315     * @param bool       $show_cousins
316     *
317     * @return Response
318     */
319    protected function ancestorsBooklet(Collection $ancestors, bool $show_cousins): Response
320    {
321        ob_start();
322
323        echo FunctionsPrint::printPedigreePerson($ancestors[1]);
324        foreach ($ancestors as $sosa => $individual) {
325            foreach ($individual->getChildFamilies() as $family) {
326                FunctionsCharts::printSosaFamily($family, $individual->xref(), $sosa, '', '', '', $show_cousins);
327            }
328        }
329
330        $html = ob_get_clean();
331
332        return new Response($html);
333    }
334
335    /**
336     * This chart can display its output in a number of styles
337     *
338     * @return array
339     */
340    protected function chartStyles(): array
341    {
342        return [
343            self::CHART_STYLE_LIST        => I18N::translate('List'),
344            self::CHART_STYLE_BOOKLET     => I18N::translate('Booklet'),
345            self::CHART_STYLE_INDIVIDUALS => I18N::translate('Individuals'),
346            self::CHART_STYLE_FAMILIES    => I18N::translate('Families'),
347        ];
348    }
349}
350