Passed
Push — master ( 01202f...713784 )
by Greg
07:38
created

PedigreeChartModule::getChartAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 27
nc 2
nop 1
dl 0
loc 38
rs 9.488
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A PedigreeChartModule::chartUrl() 0 6 1
A PedigreeChartModule::chartTitle() 0 4 1
A PedigreeChartModule::chartBoxMenu() 0 3 1
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
declare(strict_types=1);
18
19
namespace Fisharebest\Webtrees\Module;
20
21
use Aura\Router\RouterContainer;
22
use Fig\Http\Message\RequestMethodInterface;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Functions\FunctionsEdit;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Menu;
28
use Fisharebest\Webtrees\Services\ChartService;
29
use Psr\Http\Message\ResponseInterface;
30
use Psr\Http\Message\ServerRequestInterface;
31
use Psr\Http\Server\RequestHandlerInterface;
32
33
use function max;
34
use function min;
35
use function route;
36
use function view;
37
38
/**
39
 * Class PedigreeChartModule
40
 */
41
class PedigreeChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
42
{
43
    use ModuleChartTrait;
44
45
    private const ROUTE_NAME = 'pedigree-chart';
46
    private const ROUTE_URL  = '/tree/{tree}/pedigree-{style}-{generations}/{xref}';
47
48
    // Chart styles
49
    public const STYLE_LEFT  = 'left';
50
    public const STYLE_RIGHT = 'right';
51
    public const STYLE_UP    = 'up';
52
    public const STYLE_DOWN  = 'down';
53
54
    // Defaults
55
    protected const DEFAULT_GENERATIONS = '4';
56
    protected const DEFAULT_STYLE       = self::STYLE_RIGHT;
57
    protected const DEFAULT_PARAMETERS  = [
58
        'generations' => self::DEFAULT_GENERATIONS,
59
        'style'       => self::DEFAULT_STYLE,
60
    ];
61
62
    // Limits
63
    protected const MINIMUM_GENERATIONS = 2;
64
    protected const MAXIMUM_GENERATIONS = 12;
65
66
    // For RTL languages
67
    protected const MIRROR_STYLE = [
68
        self::STYLE_UP    => self::STYLE_DOWN,
69
        self::STYLE_DOWN  => self::STYLE_UP,
70
        self::STYLE_LEFT  => self::STYLE_RIGHT,
71
        self::STYLE_RIGHT => self::STYLE_LEFT,
72
    ];
73
74
    /** @var ChartService */
75
    private $chart_service;
76
77
    /**
78
     * PedigreeChartModule constructor.
79
     *
80
     * @param ChartService $chart_service
81
     */
82
    public function __construct(ChartService $chart_service)
83
    {
84
        $this->chart_service = $chart_service;
85
    }
86
87
    /**
88
     * Initialization.
89
     *
90
     * @param RouterContainer $router_container
91
     */
92
    public function boot(RouterContainer $router_container)
93
    {
94
        $router_container->getMap()
95
            ->get(self::ROUTE_NAME, self::ROUTE_URL, self::class)
96
            ->allows(RequestMethodInterface::METHOD_POST)
97
            ->tokens([
98
                'generations' => '\d+',
99
                'style'       => implode('|', array_keys($this->styles())),
100
            ]);
101
    }
102
103
    /**
104
     * How should this module be identified in the control panel, etc.?
105
     *
106
     * @return string
107
     */
108
    public function title(): string
109
    {
110
        /* I18N: Name of a module/chart */
111
        return I18N::translate('Pedigree');
112
    }
113
114
    /**
115
     * A sentence describing what this module does.
116
     *
117
     * @return string
118
     */
119
    public function description(): string
120
    {
121
        /* I18N: Description of the “PedigreeChart” module */
122
        return I18N::translate('A chart of an individual’s ancestors, formatted as a tree.');
123
    }
124
125
    /**
126
     * CSS class for the URL.
127
     *
128
     * @return string
129
     */
130
    public function chartMenuClass(): string
131
    {
132
        return 'menu-chart-pedigree';
133
    }
134
135
    /**
136
     * Return a menu item for this chart - for use in individual boxes.
137
     *
138
     * @param Individual $individual
139
     *
140
     * @return Menu|null
141
     */
142
    public function chartBoxMenu(Individual $individual): ?Menu
143
    {
144
        return $this->chartMenu($individual);
145
    }
146
147
    /**
148
     * The title for a specific instance of this chart.
149
     *
150
     * @param Individual $individual
151
     *
152
     * @return string
153
     */
154
    public function chartTitle(Individual $individual): string
155
    {
156
        /* I18N: %s is an individual’s name */
157
        return I18N::translate('Pedigree tree of %s', $individual->fullName());
158
    }
159
160
    /**
161
     * The URL for a page showing chart options.
162
     *
163
     * @param Individual $individual
164
     * @param string[]   $parameters
165
     *
166
     * @return string
167
     */
168
    public function chartUrl(Individual $individual, array $parameters = []): string
169
    {
170
        return route(self::ROUTE_NAME, [
171
                'xref' => $individual->xref(),
172
                'tree' => $individual->tree()->name(),
173
            ] + $parameters + self::DEFAULT_PARAMETERS);
174
    }
175
176
    /**
177
     * @param ServerRequestInterface $request
178
     *
179
     * @return ResponseInterface
180
     */
181
    public function handle(ServerRequestInterface $request): ResponseInterface
182
    {
183
        $ajax        = $request->getQueryParams()['ajax'] ?? '';
184
        $generations = (int) $request->getAttribute('generations');
185
        $style       = $request->getAttribute('style');
186
        $tree        = $request->getAttribute('tree');
187
        $user        = $request->getAttribute('user');
188
        $xref        = $request->getAttribute('xref');
189
        $individual  = Individual::getInstance($xref, $tree);
190
191
        // Convert POST requests into GET requests for pretty URLs.
192
        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
193
            return redirect(route(self::ROUTE_NAME, [
194
                'tree'        => $request->getAttribute('tree')->name(),
195
                'xref'        => $request->getParsedBody()['xref'],
196
                'style'       => $request->getParsedBody()['style'],
197
                'generations' => $request->getParsedBody()['generations'],
198
            ]));
199
        }
200
201
        Auth::checkIndividualAccess($individual);
202
        Auth::checkComponentAccess($this, 'chart', $tree, $user);
203
204
        $generations = min($generations, self::MAXIMUM_GENERATIONS);
205
        $generations = max($generations, self::MINIMUM_GENERATIONS);
206
207
        if ($ajax === '1') {
208
            $this->layout = 'layouts/ajax';
209
210
            $ancestors = $this->chart_service->sosaStradonitzAncestors($individual, $generations);
211
212
            // Father’s ancestors link to the father’s pedigree
213
            // Mother’s ancestors link to the mother’s pedigree..
214
            $links = $ancestors->map(function (?Individual $individual, $sosa) use ($ancestors, $style, $generations): string {
215
                if ($individual instanceof Individual && $sosa >= 2 ** $generations / 2 && $individual->childFamilies()->isNotEmpty()) {
216
                    // The last row/column, and there are more generations.
217
                    if ($sosa >= 2 ** $generations * 3 / 4) {
218
                        return $this->nextLink($ancestors->get(3), $style, $generations);
219
                    }
220
221
                    return $this->nextLink($ancestors->get(2), $style, $generations);
222
                }
223
224
                // A spacer to fix the "Left" layout.
225
                return '<span class="invisible px-2">' . view('icons/arrow-' . $style) . '</span>';
226
            });
227
228
            // Root individual links to their children.
229
            $links->put(1, $this->previousLink($individual, $style, $generations));
230
231
            return $this->viewResponse('modules/pedigree-chart/chart', [
232
                'ancestors'   => $ancestors,
233
                'generations' => $generations,
234
                'style'       => $style,
235
                'layout'      => 'right',
236
                'links'       => $links,
237
            ]);
238
        }
239
240
        $ajax_url = $this->chartUrl($individual, [
241
            'ajax'        => true,
242
            'generations' => $generations,
243
            'style'       => $style,
244
            'xref'        => $xref,
245
        ]);
246
247
        return $this->viewResponse('modules/pedigree-chart/page', [
248
            'ajax_url'           => $ajax_url,
249
            'generations'        => $generations,
250
            'generation_options' => $this->generationOptions(),
251
            'individual'         => $individual,
252
            'module'             => $this->name(),
253
            'style'              => $style,
254
            'styles'             => $this->styles(),
255
            'title'              => $this->chartTitle($individual),
256
        ]);
257
    }
258
259
    /**
260
     * Build a menu for the chart root individual
261
     *
262
     * @param Individual $individual
263
     * @param string     $style
264
     * @param int        $generations
265
     *
266
     * @return string
267
     */
268
    public function nextLink(Individual $individual, string $style, int $generations): string
269
    {
270
        $icon  = view('icons/arrow-' . $style);
271
        $title = $this->chartTitle($individual);
272
        $url   = $this->chartUrl($individual, [
273
            'style'       => $style,
274
            'generations' => $generations,
275
        ]);
276
277
        return '<a class="px-2" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $icon . '<span class="sr-only">' . $title . '</span></a>';
278
    }
279
280
    /**
281
     * Build a menu for the chart root individual
282
     *
283
     * @param Individual $individual
284
     * @param string     $style
285
     * @param int        $generations
286
     *
287
     * @return string
288
     */
289
    public function previousLink(Individual $individual, string $style, int $generations): string
290
    {
291
        $icon = view('icons/arrow-' . self::MIRROR_STYLE[$style]);
292
293
        $siblings = [];
294
        $spouses  = [];
295
        $children = [];
296
297
        foreach ($individual->childFamilies() as $family) {
298
            foreach ($family->children() as $child) {
299
                if ($child !== $individual) {
300
                    $siblings[] = $this->individualLink($child, $style, $generations);
301
                }
302
            }
303
        }
304
305
        foreach ($individual->spouseFamilies() as $family) {
306
            foreach ($family->spouses() as $spouse) {
307
                if ($spouse !== $individual) {
308
                    $spouses[] = $this->individualLink($spouse, $style, $generations);
309
                }
310
            }
311
312
            foreach ($family->children() as $child) {
313
                $children[] = $this->individualLink($child, $style, $generations);
314
            }
315
        }
316
317
        return view('modules/pedigree-chart/previous', [
318
            'icon'        => $icon,
319
            'individual'  => $individual,
320
            'generations' => $generations,
321
            'style'       => $style,
322
            'chart'       => $this,
323
            'siblings'    => $siblings,
324
            'spouses'     => $spouses,
325
            'children'    => $children,
326
        ]);
327
    }
328
329
    /**
330
     * @param Individual $individual
331
     * @param string     $style
332
     * @param int        $generations
333
     *
334
     * @return string
335
     */
336
    protected function individualLink(Individual $individual, string $style, int $generations): string
337
    {
338
        $text  = $individual->fullName();
339
        $title = $this->chartTitle($individual);
340
        $url   = $this->chartUrl($individual, [
341
            'style'       => $style,
342
            'generations' => $generations,
343
        ]);
344
345
        return '<a class="dropdown-item" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $text . '</a>';
346
    }
347
348
    /**
349
     * @return string[]
350
     */
351
    protected function generationOptions(): array
352
    {
353
        return FunctionsEdit::numericOptions(range(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS));
354
    }
355
356
    /**
357
     * This chart can display its output in a number of styles
358
     *
359
     * @return string[]
360
     */
361
    protected function styles(): array
362
    {
363
        return [
364
            self::STYLE_LEFT  => I18N::translate('Left'),
365
            self::STYLE_RIGHT => I18N::translate('Right'),
366
            self::STYLE_UP    => I18N::translate('Up'),
367
            self::STYLE_DOWN  => I18N::translate('Down'),
368
        ];
369
    }
370
}
371