xref: /webtrees/app/Module/PedigreeChartModule.php (revision 8343293f2ed38c29580f5d0f954f80b1addf4121)
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\Functions\FunctionsEdit;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Individual;
25use Fisharebest\Webtrees\Menu;
26use Fisharebest\Webtrees\Services\ChartService;
27use Fisharebest\Webtrees\Tree;
28use Symfony\Component\HttpFoundation\Request;
29use Symfony\Component\HttpFoundation\Response;
30
31/**
32 * Class PedigreeChartModule
33 */
34class PedigreeChartModule extends AbstractModule implements ModuleChartInterface
35{
36    use ModuleChartTrait;
37
38    // Defaults
39    protected const DEFAULT_GENERATIONS = '4';
40
41    // Limits
42    protected const MAX_GENERATIONS     = 12;
43    protected const MIN_GENERATIONS     = 2;
44
45    // Chart orientation options.  These are used to generate icons, views, etc.
46    public const ORIENTATION_LEFT  = 'left';
47    public const ORIENTATION_RIGHT = 'right';
48    public const ORIENTATION_UP    = 'up';
49    public const ORIENTATION_DOWN  = 'down';
50
51    protected const MIRROR_ORIENTATION = [
52        self::ORIENTATION_UP    => self::ORIENTATION_DOWN,
53        self::ORIENTATION_DOWN  => self::ORIENTATION_UP,
54        self::ORIENTATION_LEFT  => self::ORIENTATION_RIGHT,
55        self::ORIENTATION_RIGHT => self::ORIENTATION_LEFT,
56    ];
57
58    protected const DEFAULT_ORIENTATION = self::ORIENTATION_RIGHT;
59
60    /**
61     * How should this module be labelled on tabs, menus, etc.?
62     *
63     * @return string
64     */
65    public function title(): string
66    {
67        /* I18N: Name of a module/chart */
68        return I18N::translate('Pedigree');
69    }
70
71    /**
72     * A sentence describing what this module does.
73     *
74     * @return string
75     */
76    public function description(): string
77    {
78        /* I18N: Description of the “PedigreeChart” module */
79        return I18N::translate('A chart of an individual’s ancestors, formatted as a tree.');
80    }
81
82    /**
83     * CSS class for the URL.
84     *
85     * @return string
86     */
87    public function chartMenuClass(): string
88    {
89        return 'menu-chart-pedigree';
90    }
91
92    /**
93     * Return a menu item for this chart - for use in individual boxes.
94     *
95     * @param Individual $individual
96     *
97     * @return Menu|null
98     */
99    public function chartBoxMenu(Individual $individual): ?Menu
100    {
101        return $this->chartMenu($individual);
102    }
103
104    /**
105     * The title for a specific instance of this chart.
106     *
107     * @param Individual $individual
108     *
109     * @return string
110     */
111    public function chartTitle(Individual $individual): string
112    {
113        /* I18N: %s is an individual’s name */
114        return I18N::translate('Pedigree tree of %s', $individual->fullName());
115    }
116
117    /**
118     * A form to request the chart parameters.
119     *
120     * @param Request       $request
121     * @param Tree          $tree
122     * @param UserInterface $user
123     * @param ChartService  $chart_service
124     *
125     * @return Response
126     */
127    public function getChartAction(Request $request, Tree $tree, UserInterface $user, ChartService $chart_service): Response
128    {
129        $ajax       = (bool) $request->get('ajax');
130        $xref       = $request->get('xref', '');
131        $individual = Individual::getInstance($xref, $tree);
132
133        Auth::checkIndividualAccess($individual);
134        Auth::checkComponentAccess($this, 'chart', $tree, $user);
135
136        $orientation = $request->get('orientation', static::DEFAULT_ORIENTATION);
137        $generations = (int) $request->get('generations', static::DEFAULT_GENERATIONS);
138
139        $generations = min(static::MAX_GENERATIONS, $generations);
140        $generations = max(static::MIN_GENERATIONS, $generations);
141
142        $generation_options = $this->generationOptions();
143
144        if ($ajax) {
145            return $this->chart($individual, $orientation, $generations, $chart_service);
146        }
147
148        $ajax_url = $this->chartUrl($individual, [
149            'ajax'        => true,
150            'generations' => $generations,
151            'orientation' => $orientation,
152        ]);
153
154        return $this->viewResponse('modules/pedigree-chart/page', [
155            'ajax_url'           => $ajax_url,
156            'generations'        => $generations,
157            'generation_options' => $generation_options,
158            'individual'         => $individual,
159            'module_name'        => $this->name(),
160            'orientation'        => $orientation,
161            'orientations'       => $this->orientations(),
162            'title'              => $this->chartTitle($individual),
163        ]);
164    }
165
166    /**
167     * @param Individual   $individual
168     * @param string       $orientation
169     * @param int          $generations
170     * @param ChartService $chart_service
171     *
172     * @return Response
173     */
174    public function chart(Individual $individual, string $orientation, int $generations, ChartService $chart_service): Response
175    {
176        $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations);
177
178        // Father’s ancestors link to the father’s pedigree
179        // Mother’s ancestors link to the mother’s pedigree..
180        $links = $ancestors->map(function (?Individual $individual, $sosa) use ($ancestors, $orientation, $generations): string {
181            if ($individual instanceof Individual && $sosa >= 2 ** $generations / 2 && !empty($individual->childFamilies())) {
182                // The last row/column, and there are more generations.
183                if ($sosa >= 2 ** $generations * 3 / 4) {
184                    return $this->nextLink($ancestors->get(3), $orientation, $generations);
185                }
186
187                return $this->nextLink($ancestors->get(2), $orientation, $generations);
188            }
189
190            // A spacer to fix the "Left" layout.
191            return '<span class="invisible px-2">' . view('icons/arrow-' . $orientation) . '</span>';
192        });
193
194        // Root individual links to their children.
195        $links->put(1, $this->previousLink($individual, $orientation, $generations));
196
197        $html = view('modules/pedigree-chart/chart', [
198            'ancestors'   => $ancestors,
199            'generations' => $generations,
200            'orientation' => $orientation,
201            'layout'      => 'right',
202            'links'       => $links,
203        ]);
204
205        return new Response($html);
206    }
207
208    /**
209     * Build a menu for the chart root individual
210     *
211     * @param Individual $individual
212     * @param string     $orientation
213     * @param int        $generations
214     *
215     * @return string
216     */
217    public function nextLink(Individual $individual, string $orientation, int $generations): string
218    {
219        $icon  = view('icons/arrow-' . $orientation);
220        $title = $this->chartTitle($individual);
221        $url   = $this->chartUrl($individual, [
222            'orientation' => $orientation,
223            'generations' => $generations,
224        ]);
225
226        return '<a class="px-2" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $icon . '<span class="sr-only">' . $title . '</span></a>';
227    }
228
229    /**
230     * Build a menu for the chart root individual
231     *
232     * @param Individual $individual
233     * @param string     $orientation
234     * @param int        $generations
235     *
236     * @return string
237     */
238    public function previousLink(Individual $individual, string $orientation, int $generations): string
239    {
240        $icon = view('icons/arrow-' . self::MIRROR_ORIENTATION[$orientation]);
241
242        $siblings = [];
243        $spouses  = [];
244        $children = [];
245
246        foreach ($individual->childFamilies() as $family) {
247            foreach ($family->children() as $child) {
248                if ($child !== $individual) {
249                    $siblings[] = $this->individualLink($child, $orientation, $generations);
250                }
251            }
252        }
253
254        foreach ($individual->spouseFamilies() as $family) {
255            foreach ($family->spouses() as $spouse) {
256                if ($spouse !== $individual) {
257                    $spouses[] = $this->individualLink($spouse, $orientation, $generations);
258                }
259            }
260
261            foreach ($family->children() as $child) {
262                $children[] = $this->individualLink($child, $orientation, $generations);
263            }
264        }
265
266        return view('modules/pedigree-chart/previous', [
267            'icon'        => $icon,
268            'individual'  => $individual,
269            'generations' => $generations,
270            'orientation' => $orientation,
271            'chart'       => $this,
272            'siblings'    => $siblings,
273            'spouses'     => $spouses,
274            'children'    => $children,
275        ]);
276    }
277
278    /**
279     * @param Individual $individual
280     * @param string     $orientation
281     * @param int        $generations
282     *
283     * @return string
284     */
285    protected function individualLink(Individual $individual, string $orientation, int $generations): string
286    {
287        $text  = $individual->fullName();
288        $title = $this->chartTitle($individual);
289        $url   = $this->chartUrl($individual, [
290            'orientation' => $orientation,
291            'generations' => $generations,
292        ]);
293
294        return '<a class="dropdown-item" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $text . '</a>';
295    }
296
297    /**
298     * @return string[]
299     */
300    protected function generationOptions(): array
301    {
302        return FunctionsEdit::numericOptions(range(static::MIN_GENERATIONS, static::MAX_GENERATIONS));
303    }
304
305    /**
306     * @return string[]
307     */
308    protected function orientations(): array
309    {
310        return [
311            self::ORIENTATION_LEFT  => I18N::translate('Left'),
312            self::ORIENTATION_RIGHT => I18N::translate('Right'),
313            self::ORIENTATION_UP    => I18N::translate('Up'),
314            self::ORIENTATION_DOWN  => I18N::translate('Down'),
315        ];
316    }
317}
318