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; 30c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\PlaceLocation; 31c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\Registry; 32aca28033SGreg Roachuse Fisharebest\Webtrees\Services\ChartService; 33c9c6f2ecSGreg 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 83c9c6f2ecSGreg Roach private ChartService $chart_service; 84c9c6f2ecSGreg Roach 85c9c6f2ecSGreg Roach private LeafletJsService $leaflet_js_service; 8657ab2231SGreg Roach 8757ab2231SGreg Roach /** 8857ab2231SGreg Roach * PedigreeMapModule constructor. 8957ab2231SGreg Roach * 9057ab2231SGreg Roach * @param ChartService $chart_service 91c9c6f2ecSGreg Roach * @param LeafletJsService $leaflet_js_service 9257ab2231SGreg Roach */ 93c9c6f2ecSGreg Roach public function __construct(ChartService $chart_service, LeafletJsService $leaflet_js_service) 943976b470SGreg Roach { 9557ab2231SGreg Roach $this->chart_service = $chart_service; 96c9c6f2ecSGreg Roach $this->leaflet_js_service = $leaflet_js_service; 9757ab2231SGreg Roach } 9857ab2231SGreg Roach 99961ec755SGreg Roach /** 10071378461SGreg Roach * Initialization. 10171378461SGreg Roach * 1029e18e23bSGreg Roach * @return void 10371378461SGreg Roach */ 1049e18e23bSGreg Roach public function boot(): void 10571378461SGreg Roach { 1069e18e23bSGreg Roach $router_container = app(RouterContainer::class); 1079e18e23bSGreg Roach assert($router_container instanceof RouterContainer); 1089e18e23bSGreg Roach 10971378461SGreg Roach $router_container->getMap() 11072f04adfSGreg Roach ->get(static::class, static::ROUTE_URL, $this) 11171378461SGreg Roach ->allows(RequestMethodInterface::METHOD_POST) 11271378461SGreg Roach ->tokens([ 11371378461SGreg Roach 'generations' => '\d+', 11471378461SGreg Roach ]); 11571378461SGreg Roach } 11671378461SGreg Roach 11771378461SGreg Roach /** 1180cfd6963SGreg Roach * How should this module be identified in the control panel, etc.? 119961ec755SGreg Roach * 120961ec755SGreg Roach * @return string 121961ec755SGreg Roach */ 12249a243cbSGreg Roach public function title(): string 1231f374598SGreg Roach { 124bbb76c12SGreg Roach /* I18N: Name of a module */ 125bbb76c12SGreg Roach return I18N::translate('Pedigree map'); 1261f374598SGreg Roach } 1271f374598SGreg Roach 128961ec755SGreg Roach /** 129961ec755SGreg Roach * A sentence describing what this module does. 130961ec755SGreg Roach * 131961ec755SGreg Roach * @return string 132961ec755SGreg Roach */ 13349a243cbSGreg Roach public function description(): string 1341f374598SGreg Roach { 13571378461SGreg Roach /* I18N: Description of the “Pedigree map” module */ 136bbb76c12SGreg Roach return I18N::translate('Show the birthplace of ancestors on a map.'); 1371f374598SGreg Roach } 1381f374598SGreg Roach 1391f374598SGreg Roach /** 140377a2979SGreg Roach * CSS class for the URL. 141377a2979SGreg Roach * 142377a2979SGreg Roach * @return string 143377a2979SGreg Roach */ 144377a2979SGreg Roach public function chartMenuClass(): string 145377a2979SGreg Roach { 146377a2979SGreg Roach return 'menu-chart-pedigreemap'; 147377a2979SGreg Roach } 148377a2979SGreg Roach 149377a2979SGreg Roach /** 1501f374598SGreg Roach * Return a menu item for this chart - for use in individual boxes. 1511f374598SGreg Roach * 1521f374598SGreg Roach * @param Individual $individual 1531f374598SGreg Roach * 15449a243cbSGreg Roach * @return Menu|null 1551f374598SGreg Roach */ 156377a2979SGreg Roach public function chartBoxMenu(Individual $individual): ?Menu 1571f374598SGreg Roach { 158e6562982SGreg Roach return $this->chartMenu($individual); 159e6562982SGreg Roach } 160e6562982SGreg Roach 161e6562982SGreg Roach /** 162e6562982SGreg Roach * The title for a specific instance of this chart. 163e6562982SGreg Roach * 164e6562982SGreg Roach * @param Individual $individual 165e6562982SGreg Roach * 166e6562982SGreg Roach * @return string 167e6562982SGreg Roach */ 168e6562982SGreg Roach public function chartTitle(Individual $individual): string 169e6562982SGreg Roach { 170e6562982SGreg Roach /* I18N: %s is an individual’s name */ 17139ca88baSGreg Roach return I18N::translate('Pedigree map of %s', $individual->fullName()); 172e6562982SGreg Roach } 173e6562982SGreg Roach 174e6562982SGreg Roach /** 17571378461SGreg Roach * The URL for a page showing chart options. 176e6562982SGreg Roach * 177e6562982SGreg Roach * @param Individual $individual 178*09482a55SGreg Roach * @param array<bool|int|string|array|null> $parameters 179e6562982SGreg Roach * 180e6562982SGreg Roach * @return string 181e6562982SGreg Roach */ 182e6562982SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 183e6562982SGreg Roach { 18472f04adfSGreg Roach return route(static::class, [ 18571378461SGreg Roach 'tree' => $individual->tree()->name(), 186e6562982SGreg Roach 'xref' => $individual->xref(), 18771378461SGreg Roach ] + $parameters + self::DEFAULT_PARAMETERS); 188e6562982SGreg Roach } 189e6562982SGreg Roach 190e6562982SGreg Roach /** 1916ccdf4f0SGreg Roach * @param ServerRequestInterface $request 1921f374598SGreg Roach * 1936ccdf4f0SGreg Roach * @return ResponseInterface 1941f374598SGreg Roach */ 19598579324SDavid Drury public function handle(ServerRequestInterface $request): ResponseInterface 19698579324SDavid Drury { 19798579324SDavid Drury $tree = $request->getAttribute('tree'); 19898579324SDavid Drury assert($tree instanceof Tree); 19998579324SDavid Drury 20098579324SDavid Drury $xref = $request->getAttribute('xref'); 20198579324SDavid Drury assert(is_string($xref)); 20298579324SDavid Drury 2036b9cb339SGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 204cf9da572SGreg Roach $individual = Auth::checkIndividualAccess($individual, false, true); 20598579324SDavid Drury 20698579324SDavid Drury $user = $request->getAttribute('user'); 20798579324SDavid Drury $generations = (int) $request->getAttribute('generations'); 208ef483801SGreg Roach Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); 20998579324SDavid Drury 21098579324SDavid Drury // Convert POST requests into GET requests for pretty URLs. 21198579324SDavid Drury if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 21298579324SDavid Drury $params = (array) $request->getParsedBody(); 21398579324SDavid Drury 21498579324SDavid Drury return redirect(route(static::class, [ 21598579324SDavid Drury 'tree' => $tree->name(), 21698579324SDavid Drury 'xref' => $params['xref'], 21798579324SDavid Drury 'generations' => $params['generations'], 21898579324SDavid Drury ])); 21998579324SDavid Drury } 22098579324SDavid Drury 22198579324SDavid Drury $map = view('modules/pedigree-map/chart', [ 22298579324SDavid Drury 'data' => $this->getMapData($request), 223c9c6f2ecSGreg Roach 'leaflet_config' => $this->leaflet_js_service->config(), 22498579324SDavid Drury ]); 22598579324SDavid Drury 22698579324SDavid Drury return $this->viewResponse('modules/pedigree-map/page', [ 22798579324SDavid Drury 'module' => $this->name(), 22898579324SDavid Drury /* I18N: %s is an individual’s name */ 22998579324SDavid Drury 'title' => I18N::translate('Pedigree map of %s', $individual->fullName()), 23098579324SDavid Drury 'tree' => $tree, 23198579324SDavid Drury 'individual' => $individual, 23298579324SDavid Drury 'generations' => $generations, 23398579324SDavid Drury 'maxgenerations' => self::MAXIMUM_GENERATIONS, 23498579324SDavid Drury 'map' => $map, 23598579324SDavid Drury ]); 23698579324SDavid Drury } 23798579324SDavid Drury 23898579324SDavid Drury /** 23998579324SDavid Drury * @param ServerRequestInterface $request 24098579324SDavid Drury * 24129eb5762SGreg Roach * @return array<mixed> $geojson 24298579324SDavid Drury */ 24398579324SDavid Drury private function getMapData(ServerRequestInterface $request): array 2441f374598SGreg Roach { 24557ab2231SGreg Roach $tree = $request->getAttribute('tree'); 2464ea62551SGreg Roach assert($tree instanceof Tree); 2474ea62551SGreg Roach 2483130efd4SGreg Roach $color_count = count(self::COLORS); 2491f374598SGreg Roach 25057ab2231SGreg Roach $facts = $this->getPedigreeMapFacts($request, $this->chart_service); 2511f374598SGreg Roach 2521f374598SGreg Roach $geojson = [ 2531f374598SGreg Roach 'type' => 'FeatureCollection', 2541f374598SGreg Roach 'features' => [], 2551f374598SGreg Roach ]; 2567d988ec3SGreg Roach 2577d988ec3SGreg Roach $sosa_points = []; 2587d988ec3SGreg Roach 259620da733SGreg Roach foreach ($facts as $sosa => $fact) { 2605333da53SGreg Roach $location = new PlaceLocation($fact->place()->gedcomName()); 2618af6bbf8SGreg Roach 2628af6bbf8SGreg Roach // Use the co-ordinates from the fact (if they exist). 2638af6bbf8SGreg Roach $latitude = $fact->latitude(); 2648af6bbf8SGreg Roach $longitude = $fact->longitude(); 2658af6bbf8SGreg Roach 2668af6bbf8SGreg Roach // Use the co-ordinates from the location otherwise. 26790949315SGreg Roach if ($latitude === null || $longitude === null) { 2688af6bbf8SGreg Roach $latitude = $location->latitude(); 2698af6bbf8SGreg Roach $longitude = $location->longitude(); 2708af6bbf8SGreg Roach } 2718af6bbf8SGreg Roach 27290949315SGreg Roach if ($latitude !== null && $longitude !== null) { 2731f374598SGreg Roach $polyline = null; 274620da733SGreg Roach $sosa_points[$sosa] = [$latitude, $longitude]; 275620da733SGreg Roach $sosa_child = intdiv($sosa, 2); 2760d123f04SDavid Drury $color = self::COLORS[$sosa_child % $color_count]; 2770d123f04SDavid Drury 2780d123f04SDavid Drury if (array_key_exists($sosa_child, $sosa_points)) { 2791f374598SGreg Roach // Would like to use a GeometryCollection to hold LineStrings 2801f374598SGreg Roach // rather than generate polylines but the MarkerCluster library 2811f374598SGreg Roach // doesn't seem to like them 2821f374598SGreg Roach $polyline = [ 2831f374598SGreg Roach 'points' => [ 2840d123f04SDavid Drury $sosa_points[$sosa_child], 2858af6bbf8SGreg Roach [$latitude, $longitude], 2861f374598SGreg Roach ], 2871f374598SGreg Roach 'options' => [ 2881f374598SGreg Roach 'color' => $color, 2891f374598SGreg Roach ], 2901f374598SGreg Roach ]; 2911f374598SGreg Roach } 2921f374598SGreg Roach $geojson['features'][] = [ 2931f374598SGreg Roach 'type' => 'Feature', 294620da733SGreg Roach 'id' => $sosa, 2951f374598SGreg Roach 'geometry' => [ 2961f374598SGreg Roach 'type' => 'Point', 2978af6bbf8SGreg Roach 'coordinates' => [$longitude, $latitude], 2981f374598SGreg Roach ], 2991f374598SGreg Roach 'properties' => [ 3001f374598SGreg Roach 'polyline' => $polyline, 3013130efd4SGreg Roach 'iconcolor' => $color, 302497231cdSGreg Roach 'tooltip' => $fact->place()->gedcomName(), 303620da733SGreg Roach 'summary' => view('modules/pedigree-map/events', [ 304620da733SGreg Roach 'fact' => $fact, 305620da733SGreg Roach 'relationship' => ucfirst($this->getSosaName($sosa)), 306620da733SGreg Roach 'sosa' => $sosa, 307620da733SGreg Roach ]), 3081f374598SGreg Roach ], 3091f374598SGreg Roach ]; 3101f374598SGreg Roach } 3111f374598SGreg Roach } 3127d988ec3SGreg Roach 31398579324SDavid Drury return $geojson; 31471378461SGreg Roach } 31571378461SGreg Roach 31671378461SGreg Roach /** 31771378461SGreg Roach * @param ServerRequestInterface $request 3186ccdf4f0SGreg Roach * @param ChartService $chart_service 3196ccdf4f0SGreg Roach * 320fc26b4f6SGreg Roach * @return array<Fact> 3216ccdf4f0SGreg Roach */ 32257ab2231SGreg Roach private function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array 3236ccdf4f0SGreg Roach { 32457ab2231SGreg Roach $tree = $request->getAttribute('tree'); 3254ea62551SGreg Roach assert($tree instanceof Tree); 3264ea62551SGreg Roach 32798579324SDavid Drury $generations = (int) $request->getAttribute('generations'); 32898579324SDavid Drury $xref = $request->getAttribute('xref'); 3296b9cb339SGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 3306ccdf4f0SGreg Roach $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); 3316ccdf4f0SGreg Roach $facts = []; 3326ccdf4f0SGreg Roach foreach ($ancestors as $sosa => $person) { 3336ccdf4f0SGreg Roach if ($person->canShow()) { 334752e4449SGreg Roach $birth = $person->facts(Gedcom::BIRTH_EVENTS, true) 33519d319e7SGreg Roach ->first(static function (Fact $fact): bool { 336752e4449SGreg Roach return $fact->place()->gedcomName() !== ''; 33719d319e7SGreg Roach }); 338752e4449SGreg Roach 339752e4449SGreg Roach if ($birth instanceof Fact) { 3406ccdf4f0SGreg Roach $facts[$sosa] = $birth; 3416ccdf4f0SGreg Roach } 3426ccdf4f0SGreg Roach } 3436ccdf4f0SGreg Roach } 3446ccdf4f0SGreg Roach 3456ccdf4f0SGreg Roach return $facts; 3461f374598SGreg Roach } 3471f374598SGreg Roach 3481f374598SGreg Roach /** 34931e26437SGreg Roach * builds and returns sosa relationship name in the active language 35031e26437SGreg Roach * 35131e26437SGreg Roach * @param int $sosa Sosa number 35231e26437SGreg Roach * 35331e26437SGreg Roach * @return string 35431e26437SGreg Roach */ 35531e26437SGreg Roach private function getSosaName(int $sosa): string 35631e26437SGreg Roach { 35731e26437SGreg Roach $path = ''; 35831e26437SGreg Roach 35931e26437SGreg Roach while ($sosa > 1) { 36031e26437SGreg Roach if ($sosa % 2 === 1) { 36131e26437SGreg Roach $path = 'mot' . $path; 36231e26437SGreg Roach } else { 36331e26437SGreg Roach $path = 'fat' . $path; 36431e26437SGreg Roach } 36531e26437SGreg Roach $sosa = intdiv($sosa, 2); 36631e26437SGreg Roach } 36731e26437SGreg Roach 3686fcafd02SGreg Roach return app(RelationshipService::class)->legacyNameAlgorithm($path); 36931e26437SGreg Roach } 3701f374598SGreg Roach} 371