xref: /webtrees/app/Module/PedigreeMapModule.php (revision c9c6f2ec6e88594e58f14a6418e0de5aaa1484bd)
11f374598SGreg Roach<?php
23976b470SGreg Roach
31f374598SGreg Roach/**
41f374598SGreg Roach * webtrees: online genealogy
590949315SGreg Roach * Copyright (C) 2021 webtrees development team
61f374598SGreg Roach * This program is free software: you can redistribute it and/or modify
71f374598SGreg Roach * it under the terms of the GNU General Public License as published by
81f374598SGreg Roach * the Free Software Foundation, either version 3 of the License, or
91f374598SGreg Roach * (at your option) any later version.
101f374598SGreg Roach * This program is distributed in the hope that it will be useful,
111f374598SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
121f374598SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
131f374598SGreg Roach * GNU General Public License for more details.
141f374598SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
161f374598SGreg Roach */
17fcfa147eSGreg Roach
181f374598SGreg Roachdeclare(strict_types=1);
191f374598SGreg Roach
201f374598SGreg Roachnamespace Fisharebest\Webtrees\Module;
211f374598SGreg Roach
2271378461SGreg Roachuse Aura\Router\RouterContainer;
2371378461SGreg Roachuse Fig\Http\Message\RequestMethodInterface;
2471378461SGreg Roachuse Fisharebest\Webtrees\Auth;
251f374598SGreg Roachuse Fisharebest\Webtrees\Fact;
26752e4449SGreg Roachuse Fisharebest\Webtrees\Gedcom;
271f374598SGreg Roachuse Fisharebest\Webtrees\I18N;
281f374598SGreg Roachuse Fisharebest\Webtrees\Individual;
291f374598SGreg Roachuse Fisharebest\Webtrees\Menu;
30*c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
31*c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\Registry;
32aca28033SGreg Roachuse Fisharebest\Webtrees\Services\ChartService;
33*c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\Services\LeafletJsService;
346fcafd02SGreg Roachuse Fisharebest\Webtrees\Services\RelationshipService;
354ea62551SGreg Roachuse Fisharebest\Webtrees\Tree;
366ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
376ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3871378461SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
39752e4449SGreg Roach
409e18e23bSGreg Roachuse function app;
41c9a496a6SGreg Roachuse function array_key_exists;
429e18e23bSGreg Roachuse function assert;
43c9a496a6SGreg Roachuse function count;
44abaef046SGreg Roachuse function intdiv;
45ddeb3354SGreg Roachuse function is_string;
4611b6f9c6SGreg Roachuse function redirect;
4711b6f9c6SGreg Roachuse function route;
48c9a496a6SGreg Roachuse function ucfirst;
4971378461SGreg Roachuse function view;
501f374598SGreg Roach
511f374598SGreg Roach/**
521f374598SGreg Roach * Class PedigreeMapModule
531f374598SGreg Roach */
5471378461SGreg Roachclass PedigreeMapModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
551f374598SGreg Roach{
5649a243cbSGreg Roach    use ModuleChartTrait;
5749a243cbSGreg Roach
5872f04adfSGreg Roach    protected const ROUTE_URL = '/tree/{tree}/pedigree-map-{generations}/{xref}';
5971378461SGreg Roach
60e759aebbSGreg Roach    // Defaults
61e759aebbSGreg Roach    public const DEFAULT_GENERATIONS = '4';
6271378461SGreg Roach    public const DEFAULT_PARAMETERS  = [
6371378461SGreg Roach        'generations' => self::DEFAULT_GENERATIONS,
6471378461SGreg Roach    ];
65e759aebbSGreg Roach
66e759aebbSGreg Roach    // Limits
67e759aebbSGreg Roach    public const MAXIMUM_GENERATIONS = 10;
68e759aebbSGreg Roach
69c9a496a6SGreg Roach    // CSS colors for each generation
70e4b8cbcbSGreg Roach    private const COLORS = [
71e4b8cbcbSGreg Roach        'Red',
72e4b8cbcbSGreg Roach        'Green',
73e4b8cbcbSGreg Roach        'Blue',
74e4b8cbcbSGreg Roach        'Gold',
75e4b8cbcbSGreg Roach        'Cyan',
76e4b8cbcbSGreg Roach        'Orange',
77e4b8cbcbSGreg Roach        'DarkBlue',
78e4b8cbcbSGreg Roach        'LightGreen',
79e4b8cbcbSGreg Roach        'Magenta',
80e4b8cbcbSGreg Roach        'Brown',
81e4b8cbcbSGreg Roach    ];
82c9a496a6SGreg Roach
83c9a496a6SGreg Roach    private const DEFAULT_ZOOM = 2;
841f374598SGreg Roach
85*c9c6f2ecSGreg Roach    private ChartService $chart_service;
86*c9c6f2ecSGreg Roach
87*c9c6f2ecSGreg Roach    private LeafletJsService $leaflet_js_service;
8857ab2231SGreg Roach
8957ab2231SGreg Roach    /**
9057ab2231SGreg Roach     * PedigreeMapModule constructor.
9157ab2231SGreg Roach     *
9257ab2231SGreg Roach     * @param ChartService     $chart_service
93*c9c6f2ecSGreg Roach     * @param LeafletJsService $leaflet_js_service
9457ab2231SGreg Roach     */
95*c9c6f2ecSGreg Roach    public function __construct(ChartService $chart_service, LeafletJsService $leaflet_js_service)
963976b470SGreg Roach    {
9757ab2231SGreg Roach        $this->chart_service      = $chart_service;
98*c9c6f2ecSGreg Roach        $this->leaflet_js_service = $leaflet_js_service;
9957ab2231SGreg Roach    }
10057ab2231SGreg Roach
101961ec755SGreg Roach    /**
10271378461SGreg Roach     * Initialization.
10371378461SGreg Roach     *
1049e18e23bSGreg Roach     * @return void
10571378461SGreg Roach     */
1069e18e23bSGreg Roach    public function boot(): void
10771378461SGreg Roach    {
1089e18e23bSGreg Roach        $router_container = app(RouterContainer::class);
1099e18e23bSGreg Roach        assert($router_container instanceof RouterContainer);
1109e18e23bSGreg Roach
11171378461SGreg Roach        $router_container->getMap()
11272f04adfSGreg Roach            ->get(static::class, static::ROUTE_URL, $this)
11371378461SGreg Roach            ->allows(RequestMethodInterface::METHOD_POST)
11471378461SGreg Roach            ->tokens([
11571378461SGreg Roach                'generations' => '\d+',
11671378461SGreg Roach            ]);
11771378461SGreg Roach    }
11871378461SGreg Roach
11971378461SGreg Roach    /**
1200cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
121961ec755SGreg Roach     *
122961ec755SGreg Roach     * @return string
123961ec755SGreg Roach     */
12449a243cbSGreg Roach    public function title(): string
1251f374598SGreg Roach    {
126bbb76c12SGreg Roach        /* I18N: Name of a module */
127bbb76c12SGreg Roach        return I18N::translate('Pedigree map');
1281f374598SGreg Roach    }
1291f374598SGreg Roach
130961ec755SGreg Roach    /**
131961ec755SGreg Roach     * A sentence describing what this module does.
132961ec755SGreg Roach     *
133961ec755SGreg Roach     * @return string
134961ec755SGreg Roach     */
13549a243cbSGreg Roach    public function description(): string
1361f374598SGreg Roach    {
13771378461SGreg Roach        /* I18N: Description of the “Pedigree map” module */
138bbb76c12SGreg Roach        return I18N::translate('Show the birthplace of ancestors on a map.');
1391f374598SGreg Roach    }
1401f374598SGreg Roach
1411f374598SGreg Roach    /**
142377a2979SGreg Roach     * CSS class for the URL.
143377a2979SGreg Roach     *
144377a2979SGreg Roach     * @return string
145377a2979SGreg Roach     */
146377a2979SGreg Roach    public function chartMenuClass(): string
147377a2979SGreg Roach    {
148377a2979SGreg Roach        return 'menu-chart-pedigreemap';
149377a2979SGreg Roach    }
150377a2979SGreg Roach
151377a2979SGreg Roach    /**
1521f374598SGreg Roach     * Return a menu item for this chart - for use in individual boxes.
1531f374598SGreg Roach     *
1541f374598SGreg Roach     * @param Individual $individual
1551f374598SGreg Roach     *
15649a243cbSGreg Roach     * @return Menu|null
1571f374598SGreg Roach     */
158377a2979SGreg Roach    public function chartBoxMenu(Individual $individual): ?Menu
1591f374598SGreg Roach    {
160e6562982SGreg Roach        return $this->chartMenu($individual);
161e6562982SGreg Roach    }
162e6562982SGreg Roach
163e6562982SGreg Roach    /**
164e6562982SGreg Roach     * The title for a specific instance of this chart.
165e6562982SGreg Roach     *
166e6562982SGreg Roach     * @param Individual $individual
167e6562982SGreg Roach     *
168e6562982SGreg Roach     * @return string
169e6562982SGreg Roach     */
170e6562982SGreg Roach    public function chartTitle(Individual $individual): string
171e6562982SGreg Roach    {
172e6562982SGreg Roach        /* I18N: %s is an individual’s name */
17339ca88baSGreg Roach        return I18N::translate('Pedigree map of %s', $individual->fullName());
174e6562982SGreg Roach    }
175e6562982SGreg Roach
176e6562982SGreg Roach    /**
17771378461SGreg Roach     * The URL for a page showing chart options.
178e6562982SGreg Roach     *
179e6562982SGreg Roach     * @param Individual $individual
18059597b37SGreg Roach     * @param mixed[]    $parameters
181e6562982SGreg Roach     *
182e6562982SGreg Roach     * @return string
183e6562982SGreg Roach     */
184e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
185e6562982SGreg Roach    {
18672f04adfSGreg Roach        return route(static::class, [
18771378461SGreg Roach                'tree' => $individual->tree()->name(),
188e6562982SGreg Roach                'xref' => $individual->xref(),
18971378461SGreg Roach            ] + $parameters + self::DEFAULT_PARAMETERS);
190e6562982SGreg Roach    }
191e6562982SGreg Roach
192e6562982SGreg Roach    /**
1936ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
1941f374598SGreg Roach     *
1956ccdf4f0SGreg Roach     * @return ResponseInterface
1961f374598SGreg Roach     */
19798579324SDavid Drury    public function handle(ServerRequestInterface $request): ResponseInterface
19898579324SDavid Drury    {
19998579324SDavid Drury        $tree = $request->getAttribute('tree');
20098579324SDavid Drury        assert($tree instanceof Tree);
20198579324SDavid Drury
20298579324SDavid Drury        $xref = $request->getAttribute('xref');
20398579324SDavid Drury        assert(is_string($xref));
20498579324SDavid Drury
2056b9cb339SGreg Roach        $individual = Registry::individualFactory()->make($xref, $tree);
206cf9da572SGreg Roach        $individual = Auth::checkIndividualAccess($individual, false, true);
20798579324SDavid Drury
20898579324SDavid Drury        $user        = $request->getAttribute('user');
20998579324SDavid Drury        $generations = (int) $request->getAttribute('generations');
210ef483801SGreg Roach        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
21198579324SDavid Drury
21298579324SDavid Drury        // Convert POST requests into GET requests for pretty URLs.
21398579324SDavid Drury        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
21498579324SDavid Drury            $params = (array) $request->getParsedBody();
21598579324SDavid Drury
21698579324SDavid Drury            return redirect(route(static::class, [
21798579324SDavid Drury                'tree'        => $tree->name(),
21898579324SDavid Drury                'xref'        => $params['xref'],
21998579324SDavid Drury                'generations' => $params['generations'],
22098579324SDavid Drury            ]));
22198579324SDavid Drury        }
22298579324SDavid Drury
22398579324SDavid Drury        $map = view('modules/pedigree-map/chart', [
22498579324SDavid Drury            'data'           => $this->getMapData($request),
225*c9c6f2ecSGreg Roach            'leaflet_config' => $this->leaflet_js_service->config(),
22698579324SDavid Drury        ]);
22798579324SDavid Drury
22898579324SDavid Drury        return $this->viewResponse('modules/pedigree-map/page', [
22998579324SDavid Drury            'module'         => $this->name(),
23098579324SDavid Drury            /* I18N: %s is an individual’s name */
23198579324SDavid Drury            'title'          => I18N::translate('Pedigree map of %s', $individual->fullName()),
23298579324SDavid Drury            'tree'           => $tree,
23398579324SDavid Drury            'individual'     => $individual,
23498579324SDavid Drury            'generations'    => $generations,
23598579324SDavid Drury            'maxgenerations' => self::MAXIMUM_GENERATIONS,
23698579324SDavid Drury            'map'            => $map,
23798579324SDavid Drury        ]);
23898579324SDavid Drury    }
23998579324SDavid Drury
24098579324SDavid Drury    /**
24198579324SDavid Drury     * @param ServerRequestInterface $request
24298579324SDavid Drury     *
24329eb5762SGreg Roach     * @return array<mixed> $geojson
24498579324SDavid Drury     */
24598579324SDavid Drury    private function getMapData(ServerRequestInterface $request): array
2461f374598SGreg Roach    {
24757ab2231SGreg Roach        $tree = $request->getAttribute('tree');
2484ea62551SGreg Roach        assert($tree instanceof Tree);
2494ea62551SGreg Roach
2503130efd4SGreg Roach        $color_count = count(self::COLORS);
2511f374598SGreg Roach
25257ab2231SGreg Roach        $facts = $this->getPedigreeMapFacts($request, $this->chart_service);
2531f374598SGreg Roach
2541f374598SGreg Roach        $geojson = [
2551f374598SGreg Roach            'type'     => 'FeatureCollection',
2561f374598SGreg Roach            'features' => [],
2571f374598SGreg Roach        ];
2587d988ec3SGreg Roach
2597d988ec3SGreg Roach        $sosa_points = [];
2607d988ec3SGreg Roach
261620da733SGreg Roach        foreach ($facts as $sosa => $fact) {
2625333da53SGreg Roach            $location = new PlaceLocation($fact->place()->gedcomName());
2638af6bbf8SGreg Roach
2648af6bbf8SGreg Roach            // Use the co-ordinates from the fact (if they exist).
2658af6bbf8SGreg Roach            $latitude  = $fact->latitude();
2668af6bbf8SGreg Roach            $longitude = $fact->longitude();
2678af6bbf8SGreg Roach
2688af6bbf8SGreg Roach            // Use the co-ordinates from the location otherwise.
26990949315SGreg Roach            if ($latitude === null || $longitude === null) {
2708af6bbf8SGreg Roach                $latitude  = $location->latitude();
2718af6bbf8SGreg Roach                $longitude = $location->longitude();
2728af6bbf8SGreg Roach            }
2738af6bbf8SGreg Roach
27490949315SGreg Roach            if ($latitude !== null && $longitude !== null) {
2751f374598SGreg Roach                $polyline           = null;
276620da733SGreg Roach                $sosa_points[$sosa] = [$latitude, $longitude];
277620da733SGreg Roach                $sosa_child         = intdiv($sosa, 2);
2780d123f04SDavid Drury                $color              = self::COLORS[$sosa_child % $color_count];
2790d123f04SDavid Drury
2800d123f04SDavid Drury                if (array_key_exists($sosa_child, $sosa_points)) {
2811f374598SGreg Roach                    // Would like to use a GeometryCollection to hold LineStrings
2821f374598SGreg Roach                    // rather than generate polylines but the MarkerCluster library
2831f374598SGreg Roach                    // doesn't seem to like them
2841f374598SGreg Roach                    $polyline = [
2851f374598SGreg Roach                        'points'  => [
2860d123f04SDavid Drury                            $sosa_points[$sosa_child],
2878af6bbf8SGreg Roach                            [$latitude, $longitude],
2881f374598SGreg Roach                        ],
2891f374598SGreg Roach                        'options' => [
2901f374598SGreg Roach                            'color' => $color,
2911f374598SGreg Roach                        ],
2921f374598SGreg Roach                    ];
2931f374598SGreg Roach                }
2941f374598SGreg Roach                $geojson['features'][] = [
2951f374598SGreg Roach                    'type'       => 'Feature',
296620da733SGreg Roach                    'id'         => $sosa,
2971f374598SGreg Roach                    'geometry'   => [
2981f374598SGreg Roach                        'type'        => 'Point',
2998af6bbf8SGreg Roach                        'coordinates' => [$longitude, $latitude],
3001f374598SGreg Roach                    ],
3011f374598SGreg Roach                    'properties' => [
3021f374598SGreg Roach                        'polyline'  => $polyline,
3033130efd4SGreg Roach                        'iconcolor' => $color,
304497231cdSGreg Roach                        'tooltip'   => $fact->place()->gedcomName(),
305620da733SGreg Roach                        'summary'   => view('modules/pedigree-map/events', [
306620da733SGreg Roach                            'fact'         => $fact,
307620da733SGreg Roach                            'relationship' => ucfirst($this->getSosaName($sosa)),
308620da733SGreg Roach                            'sosa'         => $sosa,
309620da733SGreg Roach                        ]),
31090949315SGreg Roach                        'zoom'      => self::DEFAULT_ZOOM,
3111f374598SGreg Roach                    ],
3121f374598SGreg Roach                ];
3131f374598SGreg Roach            }
3141f374598SGreg Roach        }
3157d988ec3SGreg Roach
31698579324SDavid Drury        return $geojson;
31771378461SGreg Roach    }
31871378461SGreg Roach
31971378461SGreg Roach    /**
32071378461SGreg Roach     * @param ServerRequestInterface $request
3216ccdf4f0SGreg Roach     * @param ChartService           $chart_service
3226ccdf4f0SGreg Roach     *
323fc26b4f6SGreg Roach     * @return array<Fact>
3246ccdf4f0SGreg Roach     */
32557ab2231SGreg Roach    private function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array
3266ccdf4f0SGreg Roach    {
32757ab2231SGreg Roach        $tree = $request->getAttribute('tree');
3284ea62551SGreg Roach        assert($tree instanceof Tree);
3294ea62551SGreg Roach
33098579324SDavid Drury        $generations = (int) $request->getAttribute('generations');
33198579324SDavid Drury        $xref        = $request->getAttribute('xref');
3326b9cb339SGreg Roach        $individual  = Registry::individualFactory()->make($xref, $tree);
3336ccdf4f0SGreg Roach        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
3346ccdf4f0SGreg Roach        $facts       = [];
3356ccdf4f0SGreg Roach        foreach ($ancestors as $sosa => $person) {
3366ccdf4f0SGreg Roach            if ($person->canShow()) {
337752e4449SGreg Roach                $birth = $person->facts(Gedcom::BIRTH_EVENTS, true)
33819d319e7SGreg Roach                    ->first(static function (Fact $fact): bool {
339752e4449SGreg Roach                        return $fact->place()->gedcomName() !== '';
34019d319e7SGreg Roach                    });
341752e4449SGreg Roach
342752e4449SGreg Roach                if ($birth instanceof Fact) {
3436ccdf4f0SGreg Roach                    $facts[$sosa] = $birth;
3446ccdf4f0SGreg Roach                }
3456ccdf4f0SGreg Roach            }
3466ccdf4f0SGreg Roach        }
3476ccdf4f0SGreg Roach
3486ccdf4f0SGreg Roach        return $facts;
3491f374598SGreg Roach    }
3501f374598SGreg Roach
3511f374598SGreg Roach    /**
35231e26437SGreg Roach     * builds and returns sosa relationship name in the active language
35331e26437SGreg Roach     *
35431e26437SGreg Roach     * @param int $sosa Sosa number
35531e26437SGreg Roach     *
35631e26437SGreg Roach     * @return string
35731e26437SGreg Roach     */
35831e26437SGreg Roach    private function getSosaName(int $sosa): string
35931e26437SGreg Roach    {
36031e26437SGreg Roach        $path = '';
36131e26437SGreg Roach
36231e26437SGreg Roach        while ($sosa > 1) {
36331e26437SGreg Roach            if ($sosa % 2 === 1) {
36431e26437SGreg Roach                $path = 'mot' . $path;
36531e26437SGreg Roach            } else {
36631e26437SGreg Roach                $path = 'fat' . $path;
36731e26437SGreg Roach            }
36831e26437SGreg Roach            $sosa = intdiv($sosa, 2);
36931e26437SGreg Roach        }
37031e26437SGreg Roach
3716fcafd02SGreg Roach        return app(RelationshipService::class)->legacyNameAlgorithm($path);
37231e26437SGreg Roach    }
3731f374598SGreg Roach}
374