xref: /webtrees/app/Module/PedigreeMapModule.php (revision 19d319e7095703d7954023b55f1661a16d1edff3)
11f374598SGreg Roach<?php
23976b470SGreg Roach
31f374598SGreg Roach/**
41f374598SGreg Roach * webtrees: online genealogy
5*19d319e7SGreg Roach * Copyright (C) 2020 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
151f374598SGreg Roach * along with this program. If not, see <http://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;
246ccdf4f0SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
2571378461SGreg Roachuse Fisharebest\Webtrees\Auth;
261f374598SGreg Roachuse Fisharebest\Webtrees\Fact;
2731e26437SGreg Roachuse Fisharebest\Webtrees\Functions\Functions;
28752e4449SGreg Roachuse Fisharebest\Webtrees\Gedcom;
291f374598SGreg Roachuse Fisharebest\Webtrees\I18N;
301f374598SGreg Roachuse Fisharebest\Webtrees\Individual;
318af6bbf8SGreg Roachuse Fisharebest\Webtrees\Location;
321f374598SGreg Roachuse Fisharebest\Webtrees\Menu;
33aca28033SGreg Roachuse Fisharebest\Webtrees\Services\ChartService;
344ea62551SGreg Roachuse Fisharebest\Webtrees\Tree;
356ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
366ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3771378461SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
38752e4449SGreg Roach
399e18e23bSGreg Roachuse function app;
40c9a496a6SGreg Roachuse function array_key_exists;
419e18e23bSGreg Roachuse function assert;
42c9a496a6SGreg Roachuse function count;
43abaef046SGreg Roachuse function intdiv;
44ddeb3354SGreg Roachuse function is_string;
4511b6f9c6SGreg Roachuse function redirect;
46c9a496a6SGreg Roachuse function response;
4711b6f9c6SGreg Roachuse function route;
48c9a496a6SGreg Roachuse function strip_tags;
49c9a496a6SGreg Roachuse function ucfirst;
5071378461SGreg Roachuse function view;
511f374598SGreg Roach
521f374598SGreg Roach/**
531f374598SGreg Roach * Class PedigreeMapModule
541f374598SGreg Roach */
5571378461SGreg Roachclass PedigreeMapModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
561f374598SGreg Roach{
5749a243cbSGreg Roach    use ModuleChartTrait;
5849a243cbSGreg Roach
5972f04adfSGreg Roach    protected const ROUTE_URL  = '/tree/{tree}/pedigree-map-{generations}/{xref}';
6071378461SGreg Roach
61e759aebbSGreg Roach    // Defaults
62e759aebbSGreg Roach    public const DEFAULT_GENERATIONS = '4';
6371378461SGreg Roach    public const DEFAULT_PARAMETERS  = [
6471378461SGreg Roach        'generations' => self::DEFAULT_GENERATIONS,
6571378461SGreg Roach    ];
66e759aebbSGreg Roach
67e759aebbSGreg Roach    // Limits
68e759aebbSGreg Roach    public const MAXIMUM_GENERATIONS = 10;
6998579324SDavid Drury    private const MINZOOM            = 2;
70e759aebbSGreg Roach
71c9a496a6SGreg Roach    // CSS colors for each generation
72e4b8cbcbSGreg Roach    private const COLORS = [
73e4b8cbcbSGreg Roach        'Red',
74e4b8cbcbSGreg Roach        'Green',
75e4b8cbcbSGreg Roach        'Blue',
76e4b8cbcbSGreg Roach        'Gold',
77e4b8cbcbSGreg Roach        'Cyan',
78e4b8cbcbSGreg Roach        'Orange',
79e4b8cbcbSGreg Roach        'DarkBlue',
80e4b8cbcbSGreg Roach        'LightGreen',
81e4b8cbcbSGreg Roach        'Magenta',
82e4b8cbcbSGreg Roach        'Brown',
83e4b8cbcbSGreg Roach    ];
84c9a496a6SGreg Roach
85c9a496a6SGreg Roach    private const DEFAULT_ZOOM = 2;
861f374598SGreg Roach
8757ab2231SGreg Roach    /** @var ChartService */
8857ab2231SGreg Roach    private $chart_service;
8957ab2231SGreg Roach
9057ab2231SGreg Roach    /**
9157ab2231SGreg Roach     * PedigreeMapModule constructor.
9257ab2231SGreg Roach     *
9357ab2231SGreg Roach     * @param ChartService $chart_service
9457ab2231SGreg Roach     */
953976b470SGreg Roach    public function __construct(ChartService $chart_service)
963976b470SGreg Roach    {
9757ab2231SGreg Roach        $this->chart_service = $chart_service;
9857ab2231SGreg Roach    }
9957ab2231SGreg Roach
100961ec755SGreg Roach    /**
10171378461SGreg Roach     * Initialization.
10271378461SGreg Roach     *
1039e18e23bSGreg Roach     * @return void
10471378461SGreg Roach     */
1059e18e23bSGreg Roach    public function boot(): void
10671378461SGreg Roach    {
1079e18e23bSGreg Roach        $router_container = app(RouterContainer::class);
1089e18e23bSGreg Roach        assert($router_container instanceof RouterContainer);
1099e18e23bSGreg Roach
11071378461SGreg Roach        $router_container->getMap()
11172f04adfSGreg Roach            ->get(static::class, static::ROUTE_URL, $this)
11271378461SGreg Roach            ->allows(RequestMethodInterface::METHOD_POST)
11371378461SGreg Roach            ->tokens([
11471378461SGreg Roach                'generations' => '\d+',
11571378461SGreg Roach            ]);
11671378461SGreg Roach    }
11771378461SGreg Roach
11871378461SGreg Roach    /**
1190cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
120961ec755SGreg Roach     *
121961ec755SGreg Roach     * @return string
122961ec755SGreg Roach     */
12349a243cbSGreg Roach    public function title(): string
1241f374598SGreg Roach    {
125bbb76c12SGreg Roach        /* I18N: Name of a module */
126bbb76c12SGreg Roach        return I18N::translate('Pedigree map');
1271f374598SGreg Roach    }
1281f374598SGreg Roach
129961ec755SGreg Roach    /**
130961ec755SGreg Roach     * A sentence describing what this module does.
131961ec755SGreg Roach     *
132961ec755SGreg Roach     * @return string
133961ec755SGreg Roach     */
13449a243cbSGreg Roach    public function description(): string
1351f374598SGreg Roach    {
13671378461SGreg Roach        /* I18N: Description of the “Pedigree map” module */
137bbb76c12SGreg Roach        return I18N::translate('Show the birthplace of ancestors on a map.');
1381f374598SGreg Roach    }
1391f374598SGreg Roach
1401f374598SGreg Roach    /**
141377a2979SGreg Roach     * CSS class for the URL.
142377a2979SGreg Roach     *
143377a2979SGreg Roach     * @return string
144377a2979SGreg Roach     */
145377a2979SGreg Roach    public function chartMenuClass(): string
146377a2979SGreg Roach    {
147377a2979SGreg Roach        return 'menu-chart-pedigreemap';
148377a2979SGreg Roach    }
149377a2979SGreg Roach
150377a2979SGreg Roach    /**
1511f374598SGreg Roach     * Return a menu item for this chart - for use in individual boxes.
1521f374598SGreg Roach     *
1531f374598SGreg Roach     * @param Individual $individual
1541f374598SGreg Roach     *
15549a243cbSGreg Roach     * @return Menu|null
1561f374598SGreg Roach     */
157377a2979SGreg Roach    public function chartBoxMenu(Individual $individual): ?Menu
1581f374598SGreg Roach    {
159e6562982SGreg Roach        return $this->chartMenu($individual);
160e6562982SGreg Roach    }
161e6562982SGreg Roach
162e6562982SGreg Roach    /**
163e6562982SGreg Roach     * The title for a specific instance of this chart.
164e6562982SGreg Roach     *
165e6562982SGreg Roach     * @param Individual $individual
166e6562982SGreg Roach     *
167e6562982SGreg Roach     * @return string
168e6562982SGreg Roach     */
169e6562982SGreg Roach    public function chartTitle(Individual $individual): string
170e6562982SGreg Roach    {
171e6562982SGreg Roach        /* I18N: %s is an individual’s name */
17239ca88baSGreg Roach        return I18N::translate('Pedigree map of %s', $individual->fullName());
173e6562982SGreg Roach    }
174e6562982SGreg Roach
175e6562982SGreg Roach    /**
17671378461SGreg Roach     * The URL for a page showing chart options.
177e6562982SGreg Roach     *
178e6562982SGreg Roach     * @param Individual $individual
17959597b37SGreg Roach     * @param mixed[]    $parameters
180e6562982SGreg Roach     *
181e6562982SGreg Roach     * @return string
182e6562982SGreg Roach     */
183e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
184e6562982SGreg Roach    {
18572f04adfSGreg Roach        return route(static::class, [
18671378461SGreg Roach                'tree' => $individual->tree()->name(),
187e6562982SGreg Roach                'xref' => $individual->xref(),
18871378461SGreg Roach            ] + $parameters + self::DEFAULT_PARAMETERS);
189e6562982SGreg Roach    }
190e6562982SGreg Roach
191e6562982SGreg Roach    /**
1926ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
1931f374598SGreg Roach     *
1946ccdf4f0SGreg Roach     * @return ResponseInterface
1951f374598SGreg Roach     */
19698579324SDavid Drury    public function handle(ServerRequestInterface $request): ResponseInterface
19798579324SDavid Drury    {
19898579324SDavid Drury        $tree = $request->getAttribute('tree');
19998579324SDavid Drury        assert($tree instanceof Tree);
20098579324SDavid Drury
20198579324SDavid Drury        $xref = $request->getAttribute('xref');
20298579324SDavid Drury        assert(is_string($xref));
20398579324SDavid Drury
20498579324SDavid Drury        $individual  = Individual::getInstance($xref, $tree);
205cf9da572SGreg Roach        $individual  = Auth::checkIndividualAccess($individual, false, true);
20698579324SDavid Drury
20798579324SDavid Drury        $user        = $request->getAttribute('user');
20898579324SDavid Drury        $generations = (int) $request->getAttribute('generations');
20998579324SDavid Drury        Auth::checkComponentAccess($this, 'chart', $tree, $user);
21098579324SDavid Drury
21198579324SDavid Drury        // Convert POST requests into GET requests for pretty URLs.
21298579324SDavid Drury        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
21398579324SDavid Drury            $params = (array) $request->getParsedBody();
21498579324SDavid Drury
21598579324SDavid Drury            return redirect(route(static::class, [
21698579324SDavid Drury                'tree'        => $tree->name(),
21798579324SDavid Drury                'xref'        => $params['xref'],
21898579324SDavid Drury                'generations' => $params['generations'],
21998579324SDavid Drury            ]));
22098579324SDavid Drury        }
22198579324SDavid Drury
22298579324SDavid Drury        $map = view('modules/pedigree-map/chart', [
22398579324SDavid Drury            'data'     => $this->getMapData($request),
22498579324SDavid Drury            'provider' => [
2252cbb0620SDavid Drury                'url'    => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
2262cbb0620SDavid Drury                'options' => [
2272cbb0620SDavid Drury                    'attribution' => '<a href="https://www.openstreetmap.org/copyright">&copy; OpenStreetMap</a> contributors',
2282cbb0620SDavid Drury                    'max_zoom'    => 19
2292cbb0620SDavid Drury                ]
23098579324SDavid Drury            ]
23198579324SDavid Drury        ]);
23298579324SDavid Drury
23398579324SDavid Drury        return $this->viewResponse('modules/pedigree-map/page', [
23498579324SDavid Drury            'module'         => $this->name(),
23598579324SDavid Drury            /* I18N: %s is an individual’s name */
23698579324SDavid Drury            'title'          => I18N::translate('Pedigree map of %s', $individual->fullName()),
23798579324SDavid Drury            'tree'           => $tree,
23898579324SDavid Drury            'individual'     => $individual,
23998579324SDavid Drury            'generations'    => $generations,
24098579324SDavid Drury            'maxgenerations' => self::MAXIMUM_GENERATIONS,
24198579324SDavid Drury            'map'            => $map,
24298579324SDavid Drury        ]);
24398579324SDavid Drury    }
24498579324SDavid Drury
24598579324SDavid Drury    /**
24698579324SDavid Drury     * @param ServerRequestInterface $request
24798579324SDavid Drury     *
24829eb5762SGreg Roach     * @return array<mixed> $geojson
24998579324SDavid Drury     */
25098579324SDavid Drury    private function getMapData(ServerRequestInterface $request): array
2511f374598SGreg Roach    {
25257ab2231SGreg Roach        $tree = $request->getAttribute('tree');
2534ea62551SGreg Roach        assert($tree instanceof Tree);
2544ea62551SGreg Roach
2553130efd4SGreg Roach        $color_count = count(self::COLORS);
2561f374598SGreg Roach
25757ab2231SGreg Roach        $facts = $this->getPedigreeMapFacts($request, $this->chart_service);
2581f374598SGreg Roach
2591f374598SGreg Roach        $geojson = [
2601f374598SGreg Roach            'type'     => 'FeatureCollection',
2611f374598SGreg Roach            'features' => [],
2621f374598SGreg Roach        ];
2637d988ec3SGreg Roach
2647d988ec3SGreg Roach        $sosa_points = [];
2657d988ec3SGreg Roach
266620da733SGreg Roach        foreach ($facts as $sosa => $fact) {
2678af6bbf8SGreg Roach            $location = new Location($fact->place()->gedcomName());
2688af6bbf8SGreg Roach
2698af6bbf8SGreg Roach            // Use the co-ordinates from the fact (if they exist).
2708af6bbf8SGreg Roach            $latitude  = $fact->latitude();
2718af6bbf8SGreg Roach            $longitude = $fact->longitude();
2728af6bbf8SGreg Roach
2738af6bbf8SGreg Roach            // Use the co-ordinates from the location otherwise.
2748af6bbf8SGreg Roach            if ($latitude === 0.0 && $longitude === 0.0) {
2758af6bbf8SGreg Roach                $latitude  = $location->latitude();
2768af6bbf8SGreg Roach                $longitude = $location->longitude();
2778af6bbf8SGreg Roach            }
2788af6bbf8SGreg Roach
2798af6bbf8SGreg Roach            if ($latitude !== 0.0 || $longitude !== 0.0) {
2801f374598SGreg Roach                $polyline           = null;
281620da733SGreg Roach                $sosa_points[$sosa] = [$latitude, $longitude];
282620da733SGreg Roach                $sosa_child         = intdiv($sosa, 2);
2830d123f04SDavid Drury                $color              = self::COLORS[$sosa_child % $color_count];
2840d123f04SDavid Drury
2850d123f04SDavid Drury                if (array_key_exists($sosa_child, $sosa_points)) {
2861f374598SGreg Roach                    // Would like to use a GeometryCollection to hold LineStrings
2871f374598SGreg Roach                    // rather than generate polylines but the MarkerCluster library
2881f374598SGreg Roach                    // doesn't seem to like them
2891f374598SGreg Roach                    $polyline = [
2901f374598SGreg Roach                        'points'  => [
2910d123f04SDavid Drury                            $sosa_points[$sosa_child],
2928af6bbf8SGreg Roach                            [$latitude, $longitude],
2931f374598SGreg Roach                        ],
2941f374598SGreg Roach                        'options' => [
2951f374598SGreg Roach                            'color' => $color,
2961f374598SGreg Roach                        ],
2971f374598SGreg Roach                    ];
2981f374598SGreg Roach                }
2991f374598SGreg Roach                $geojson['features'][] = [
3001f374598SGreg Roach                    'type'       => 'Feature',
301620da733SGreg Roach                    'id'         => $sosa,
3021f374598SGreg Roach                    'geometry'   => [
3031f374598SGreg Roach                        'type'        => 'Point',
3048af6bbf8SGreg Roach                        'coordinates' => [$longitude, $latitude],
3051f374598SGreg Roach                    ],
3061f374598SGreg Roach                    'properties' => [
3071f374598SGreg Roach                        'polyline'  => $polyline,
3083130efd4SGreg Roach                        'iconcolor' => $color,
3098af6bbf8SGreg Roach                        'tooltip'   => strip_tags($fact->place()->fullName()),
310620da733SGreg Roach                        'summary'   => view('modules/pedigree-map/events', [
311620da733SGreg Roach                            'fact'         => $fact,
312620da733SGreg Roach                            'relationship' => ucfirst($this->getSosaName($sosa)),
313620da733SGreg Roach                            'sosa'         => $sosa,
314620da733SGreg Roach                        ]),
315c9a496a6SGreg Roach                        'zoom'      => $location->zoom() ?: self::DEFAULT_ZOOM,
3161f374598SGreg Roach                    ],
3171f374598SGreg Roach                ];
3181f374598SGreg Roach            }
3191f374598SGreg Roach        }
3207d988ec3SGreg Roach
32198579324SDavid Drury        return $geojson;
32271378461SGreg Roach    }
32371378461SGreg Roach
32471378461SGreg Roach    /**
32571378461SGreg Roach     * @param ServerRequestInterface $request
3266ccdf4f0SGreg Roach     * @param ChartService           $chart_service
3276ccdf4f0SGreg Roach     *
3286ccdf4f0SGreg Roach     * @return array
3296ccdf4f0SGreg Roach     */
33057ab2231SGreg Roach    private function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array
3316ccdf4f0SGreg Roach    {
33257ab2231SGreg Roach        $tree = $request->getAttribute('tree');
3334ea62551SGreg Roach        assert($tree instanceof Tree);
3344ea62551SGreg Roach
33598579324SDavid Drury        $generations = (int) $request->getAttribute('generations');
33698579324SDavid Drury        $xref        = $request->getAttribute('xref');
3376ccdf4f0SGreg Roach        $individual  = Individual::getInstance($xref, $tree);
3386ccdf4f0SGreg Roach        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
3396ccdf4f0SGreg Roach        $facts       = [];
3406ccdf4f0SGreg Roach        foreach ($ancestors as $sosa => $person) {
3416ccdf4f0SGreg Roach            if ($person->canShow()) {
342752e4449SGreg Roach                $birth = $person->facts(Gedcom::BIRTH_EVENTS, true)
343*19d319e7SGreg Roach                    ->first(static function (Fact $fact): bool {
344752e4449SGreg Roach                        return $fact->place()->gedcomName() !== '';
345*19d319e7SGreg Roach                    });
346752e4449SGreg Roach
347752e4449SGreg Roach                if ($birth instanceof Fact) {
3486ccdf4f0SGreg Roach                    $facts[$sosa] = $birth;
3496ccdf4f0SGreg Roach                }
3506ccdf4f0SGreg Roach            }
3516ccdf4f0SGreg Roach        }
3526ccdf4f0SGreg Roach
3536ccdf4f0SGreg Roach        return $facts;
3541f374598SGreg Roach    }
3551f374598SGreg Roach
3561f374598SGreg Roach    /**
35731e26437SGreg Roach     * builds and returns sosa relationship name in the active language
35831e26437SGreg Roach     *
35931e26437SGreg Roach     * @param int $sosa Sosa number
36031e26437SGreg Roach     *
36131e26437SGreg Roach     * @return string
36231e26437SGreg Roach     */
36331e26437SGreg Roach    private function getSosaName(int $sosa): string
36431e26437SGreg Roach    {
36531e26437SGreg Roach        $path = '';
36631e26437SGreg Roach
36731e26437SGreg Roach        while ($sosa > 1) {
36831e26437SGreg Roach            if ($sosa % 2 === 1) {
36931e26437SGreg Roach                $path = 'mot' . $path;
37031e26437SGreg Roach            } else {
37131e26437SGreg Roach                $path = 'fat' . $path;
37231e26437SGreg Roach            }
37331e26437SGreg Roach            $sosa = intdiv($sosa, 2);
37431e26437SGreg Roach        }
37531e26437SGreg Roach
37631e26437SGreg Roach        return Functions::getRelationshipNameFromPath($path);
37731e26437SGreg Roach    }
3781f374598SGreg Roach}
379