Passed
Push — master ( 01221f...bb1ec7 )
by Greg
06:12
created

PedigreeChartModule::chartBoxMenu()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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