Completed
Push — master ( 1792ff...620da7 )
by Greg
16:52 queued 11:02
created

PedigreeMapModule   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 315
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 129
c 3
b 0
f 0
dl 0
loc 315
rs 10
wmc 27

12 Methods

Rating   Name   Duplication   Size   Complexity  
A chartUrl() 0 6 1
A description() 0 4 1
A title() 0 4 1
A chartMenuClass() 0 3 1
A boot() 0 10 1
A chartTitle() 0 4 1
A __construct() 0 3 1
A chartBoxMenu() 0 3 1
A getPedigreeMapFacts() 0 20 5
B getMapDataAction() 0 76 9
A getSosaName() 0 14 3
A handle() 0 40 2
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 Fig\Http\Message\StatusCodeInterface;
25
use Fisharebest\Webtrees\Auth;
26
use Fisharebest\Webtrees\Fact;
27
use Fisharebest\Webtrees\Functions\Functions;
28
use Fisharebest\Webtrees\I18N;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Location;
31
use Fisharebest\Webtrees\Menu;
32
use Fisharebest\Webtrees\Services\ChartService;
33
use Fisharebest\Webtrees\Tree;
34
use Psr\Http\Message\ResponseInterface;
35
use Psr\Http\Message\ServerRequestInterface;
36
use Psr\Http\Server\RequestHandlerInterface;
37
use function app;
38
use function array_key_exists;
39
use function assert;
40
use function count;
41
use function intdiv;
42
use function is_string;
43
use function redirect;
44
use function response;
45
use function route;
46
use function strip_tags;
47
use function ucfirst;
48
use function view;
49
50
/**
51
 * Class PedigreeMapModule
52
 */
53
class PedigreeMapModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
54
{
55
    use ModuleChartTrait;
56
57
    protected const ROUTE_URL  = '/tree/{tree}/pedigree-map-{generations}/{xref}';
58
59
    // Defaults
60
    public const DEFAULT_GENERATIONS = '4';
61
    public const DEFAULT_PARAMETERS  = [
62
        'generations' => self::DEFAULT_GENERATIONS,
63
    ];
64
65
    // Limits
66
    public const MAXIMUM_GENERATIONS = 10;
67
68
    // CSS colors for each generation
69
    private const COLORS = [
70
        'Red',
71
        'Green',
72
        'Blue',
73
        'Gold',
74
        'Cyan',
75
        'Orange',
76
        'DarkBlue',
77
        'LightGreen',
78
        'Magenta',
79
        'Brown',
80
    ];
81
82
    private const DEFAULT_ZOOM = 2;
83
84
    /** @var ChartService */
85
    private $chart_service;
86
87
    /**
88
     * PedigreeMapModule constructor.
89
     *
90
     * @param ChartService $chart_service
91
     */
92
    public function __construct(ChartService $chart_service)
93
    {
94
        $this->chart_service = $chart_service;
95
    }
96
97
    /**
98
     * Initialization.
99
     *
100
     * @return void
101
     */
102
    public function boot(): void
103
    {
104
        $router_container = app(RouterContainer::class);
105
        assert($router_container instanceof RouterContainer);
106
107
        $router_container->getMap()
108
            ->get(static::class, static::ROUTE_URL, $this)
109
            ->allows(RequestMethodInterface::METHOD_POST)
110
            ->tokens([
111
                'generations' => '\d+',
112
            ]);
113
    }
114
115
    /**
116
     * How should this module be identified in the control panel, etc.?
117
     *
118
     * @return string
119
     */
120
    public function title(): string
121
    {
122
        /* I18N: Name of a module */
123
        return I18N::translate('Pedigree map');
124
    }
125
126
    /**
127
     * A sentence describing what this module does.
128
     *
129
     * @return string
130
     */
131
    public function description(): string
132
    {
133
        /* I18N: Description of the “Pedigree map” module */
134
        return I18N::translate('Show the birthplace of ancestors on a map.');
135
    }
136
137
    /**
138
     * CSS class for the URL.
139
     *
140
     * @return string
141
     */
142
    public function chartMenuClass(): string
143
    {
144
        return 'menu-chart-pedigreemap';
145
    }
146
147
    /**
148
     * Return a menu item for this chart - for use in individual boxes.
149
     *
150
     * @param Individual $individual
151
     *
152
     * @return Menu|null
153
     */
154
    public function chartBoxMenu(Individual $individual): ?Menu
155
    {
156
        return $this->chartMenu($individual);
157
    }
158
159
    /**
160
     * The title for a specific instance of this chart.
161
     *
162
     * @param Individual $individual
163
     *
164
     * @return string
165
     */
166
    public function chartTitle(Individual $individual): string
167
    {
168
        /* I18N: %s is an individual’s name */
169
        return I18N::translate('Pedigree map of %s', $individual->fullName());
170
    }
171
172
    /**
173
     * The URL for a page showing chart options.
174
     *
175
     * @param Individual $individual
176
     * @param mixed[]    $parameters
177
     *
178
     * @return string
179
     */
180
    public function chartUrl(Individual $individual, array $parameters = []): string
181
    {
182
        return route(static::class, [
183
                'tree' => $individual->tree()->name(),
184
                'xref' => $individual->xref(),
185
            ] + $parameters + self::DEFAULT_PARAMETERS);
186
    }
187
188
    /**
189
     * @param ServerRequestInterface $request
190
     *
191
     * @return ResponseInterface
192
     */
193
    public function getMapDataAction(ServerRequestInterface $request): ResponseInterface
194
    {
195
        $tree = $request->getAttribute('tree');
196
        assert($tree instanceof Tree);
197
198
        $xref        = $request->getQueryParams()['xref'];
199
        $individual  = Individual::getInstance($xref, $tree);
200
        $color_count = count(self::COLORS);
201
202
        $facts = $this->getPedigreeMapFacts($request, $this->chart_service);
203
204
        $geojson = [
205
            'type'     => 'FeatureCollection',
206
            'features' => [],
207
        ];
208
209
        $sosa_points = [];
210
211
        foreach ($facts as $sosa => $fact) {
212
            $location = new Location($fact->place()->gedcomName());
213
214
            // Use the co-ordinates from the fact (if they exist).
215
            $latitude  = $fact->latitude();
216
            $longitude = $fact->longitude();
217
218
            // Use the co-ordinates from the location otherwise.
219
            if ($latitude === 0.0 && $longitude === 0.0) {
220
                $latitude  = $location->latitude();
221
                $longitude = $location->longitude();
222
            }
223
224
            if ($latitude !== 0.0 || $longitude !== 0.0) {
225
                $polyline           = null;
226
                $sosa_points[$sosa] = [$latitude, $longitude];
227
                $sosa_child         = intdiv($sosa, 2);
228
                $color              = self::COLORS[$sosa_child % $color_count];
229
230
                if (array_key_exists($sosa_child, $sosa_points)) {
231
                    // Would like to use a GeometryCollection to hold LineStrings
232
                    // rather than generate polylines but the MarkerCluster library
233
                    // doesn't seem to like them
234
                    $polyline = [
235
                        'points'  => [
236
                            $sosa_points[$sosa_child],
237
                            [$latitude, $longitude],
238
                        ],
239
                        'options' => [
240
                            'color' => $color,
241
                        ],
242
                    ];
243
                }
244
                $geojson['features'][] = [
245
                    'type'       => 'Feature',
246
                    'id'         => $sosa,
247
                    'geometry'   => [
248
                        'type'        => 'Point',
249
                        'coordinates' => [$longitude, $latitude],
250
                    ],
251
                    'properties' => [
252
                        'polyline'  => $polyline,
253
                        'iconcolor' => $color,
254
                        'tooltip'   => strip_tags($fact->place()->fullName()),
255
                        'summary'   => view('modules/pedigree-map/events', [
256
                            'fact'         => $fact,
257
                            'relationship' => ucfirst($this->getSosaName($sosa)),
258
                            'sosa'         => $sosa,
259
                        ]),
260
                        'zoom'      => $location->zoom() ?: self::DEFAULT_ZOOM,
261
                    ],
262
                ];
263
            }
264
        }
265
266
        $code = $facts === [] ? StatusCodeInterface::STATUS_NO_CONTENT : StatusCodeInterface::STATUS_OK;
267
268
        return response($geojson, $code);
269
    }
270
271
    /**
272
     * @param ServerRequestInterface $request
273
     *
274
     * @return ResponseInterface
275
     */
276
    public function handle(ServerRequestInterface $request): ResponseInterface
277
    {
278
        $tree = $request->getAttribute('tree');
279
        assert($tree instanceof Tree);
280
281
        $xref = $request->getAttribute('xref');
282
        assert(is_string($xref));
283
284
        $individual  = Individual::getInstance($xref, $tree);
285
        $individual  = Auth::checkIndividualAccess($individual);
286
287
        $user        = $request->getAttribute('user');
288
        $generations = (int) $request->getAttribute('generations');
289
        Auth::checkComponentAccess($this, 'chart', $tree, $user);
290
291
        // Convert POST requests into GET requests for pretty URLs.
292
        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
293
            $params = (array) $request->getParsedBody();
294
295
            return redirect(route(static::class, [
296
                'tree'        => $tree->name(),
297
                'xref'        => $params['xref'],
298
                'generations' => $params['generations'],
299
            ]));
300
        }
301
302
        $map = view('modules/pedigree-map/chart', [
303
            'individual'  => $individual,
304
            'generations' => $generations,
305
        ]);
306
307
        return $this->viewResponse('modules/pedigree-map/page', [
308
            'module'         => $this->name(),
309
            /* I18N: %s is an individual’s name */
310
            'title'          => I18N::translate('Pedigree map of %s', $individual->fullName()),
311
            'tree'           => $tree,
312
            'individual'     => $individual,
313
            'generations'    => $generations,
314
            'maxgenerations' => self::MAXIMUM_GENERATIONS,
315
            'map'            => $map,
316
        ]);
317
    }
318
319
    /**
320
     * @param ServerRequestInterface $request
321
     * @param ChartService           $chart_service
322
     *
323
     * @return array
324
     */
325
    private function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array
326
    {
327
        $tree = $request->getAttribute('tree');
328
        assert($tree instanceof Tree);
329
330
        $generations = (int) $request->getQueryParams()['generations'];
331
        $xref        = $request->getQueryParams()['xref'];
332
        $individual  = Individual::getInstance($xref, $tree);
333
        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
334
        $facts       = [];
335
        foreach ($ancestors as $sosa => $person) {
336
            if ($person->canShow()) {
337
                $birth = $person->facts(['BIRT'])->first();
338
                if ($birth instanceof Fact && $birth->place()->gedcomName() !== '') {
339
                    $facts[$sosa] = $birth;
340
                }
341
            }
342
        }
343
344
        return $facts;
345
    }
346
347
    /**
348
     * builds and returns sosa relationship name in the active language
349
     *
350
     * @param int $sosa Sosa number
351
     *
352
     * @return string
353
     */
354
    private function getSosaName(int $sosa): string
355
    {
356
        $path = '';
357
358
        while ($sosa > 1) {
359
            if ($sosa % 2 === 1) {
360
                $path = 'mot' . $path;
361
            } else {
362
                $path = 'fat' . $path;
363
            }
364
            $sosa = intdiv($sosa, 2);
365
        }
366
367
        return Functions::getRelationshipNameFromPath($path);
368
    }
369
}
370