. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Module; use Fig\Http\Message\RequestMethodInterface; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Fact; use Fisharebest\Webtrees\Gedcom; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Menu; use Fisharebest\Webtrees\PlaceLocation; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Services\ChartService; use Fisharebest\Webtrees\Services\LeafletJsService; use Fisharebest\Webtrees\Services\RelationshipService; use Fisharebest\Webtrees\Validator; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use function array_key_exists; use function intdiv; use function redirect; use function route; use function ucfirst; use function view; /** * Class PedigreeMapModule */ class PedigreeMapModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface { use ModuleChartTrait; protected const ROUTE_URL = '/tree/{tree}/pedigree-map-{generations}/{xref}'; // Defaults public const DEFAULT_GENERATIONS = '4'; public const DEFAULT_PARAMETERS = [ 'generations' => self::DEFAULT_GENERATIONS, ]; // Limits public const MINIMUM_GENERATIONS = 1; public const MAXIMUM_GENERATIONS = 10; // CSS colors for each generation protected const COUNT_CSS_COLORS = 12; protected ChartService $chart_service; protected LeafletJsService $leaflet_js_service; protected RelationshipService $relationship_service; /** * @param ChartService $chart_service * @param LeafletJsService $leaflet_js_service * @param RelationshipService $relationship_service */ public function __construct( ChartService $chart_service, LeafletJsService $leaflet_js_service, RelationshipService $relationship_service ) { $this->chart_service = $chart_service; $this->leaflet_js_service = $leaflet_js_service; $this->relationship_service = $relationship_service; } /** * Initialization. * * @return void */ public function boot(): void { Registry::routeFactory()->routeMap() ->get(static::class, static::ROUTE_URL, $this) ->allows(RequestMethodInterface::METHOD_POST); } /** * How should this module be identified in the control panel, etc.? * * @return string */ public function title(): string { /* I18N: Name of a module */ return I18N::translate('Pedigree map'); } public function description(): string { /* I18N: Description of the “Pedigree map” module */ return I18N::translate('Show the birthplace of ancestors on a map.'); } /** * CSS class for the URL. * * @return string */ public function chartMenuClass(): string { return 'menu-chart-pedigreemap'; } /** * Return a menu item for this chart - for use in individual boxes. * * @param Individual $individual * * @return Menu|null */ public function chartBoxMenu(Individual $individual): Menu|null { return $this->chartMenu($individual); } /** * The title for a specific instance of this chart. * * @param Individual $individual * * @return string */ public function chartTitle(Individual $individual): string { /* I18N: %s is an individual’s name */ return I18N::translate('Pedigree map of %s', $individual->fullName()); } /** * The URL for a page showing chart options. * * @param Individual $individual * @param array|null> $parameters * * @return string */ public function chartUrl(Individual $individual, array $parameters = []): string { return route(static::class, [ 'tree' => $individual->tree()->name(), 'xref' => $individual->xref(), ] + $parameters + self::DEFAULT_PARAMETERS); } /** * @param ServerRequestInterface $request * * @return ResponseInterface */ public function handle(ServerRequestInterface $request): ResponseInterface { $tree = Validator::attributes($request)->tree(); $user = Validator::attributes($request)->user(); $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'); $xref = Validator::attributes($request)->isXref()->string('xref'); // Convert POST requests into GET requests for pretty URLs. if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { return redirect(route(static::class, [ 'tree' => $tree->name(), 'xref' => Validator::parsedBody($request)->isXref()->string('xref'), 'generations' => Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'), ])); } Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); $individual = Registry::individualFactory()->make($xref, $tree); $individual = Auth::checkIndividualAccess($individual, false, true); $map = view('modules/pedigree-map/chart', [ 'data' => $this->getMapData($request), 'leaflet_config' => $this->leaflet_js_service->config(), ]); return $this->viewResponse('modules/pedigree-map/page', [ 'module' => $this->name(), /* I18N: %s is an individual’s name */ 'title' => I18N::translate('Pedigree map of %s', $individual->fullName()), 'tree' => $tree, 'individual' => $individual, 'generations' => $generations, 'maxgenerations' => self::MAXIMUM_GENERATIONS, 'map' => $map, ]); } /** * @param ServerRequestInterface $request * * @return array $geojson */ protected function getMapData(ServerRequestInterface $request): array { $facts = $this->getPedigreeMapFacts($request, $this->chart_service); $geojson = [ 'type' => 'FeatureCollection', 'features' => [], ]; $sosa_points = []; foreach ($facts as $sosa => $fact) { $location = new PlaceLocation($fact->place()->gedcomName()); // Use the co-ordinates from the fact (if they exist). $latitude = $fact->latitude(); $longitude = $fact->longitude(); // Use the co-ordinates from the location otherwise. if ($latitude === null || $longitude === null) { $latitude = $location->latitude(); $longitude = $location->longitude(); } if ($latitude !== null && $longitude !== null) { $polyline = null; $sosa_points[$sosa] = [$latitude, $longitude]; $sosa_child = intdiv($sosa, 2); $generation = (int) log($sosa, 2); $color = 'var(--wt-pedigree-map-gen-' . $generation % self::COUNT_CSS_COLORS . ')'; $class = 'wt-pedigree-map-gen-' . $generation % self::COUNT_CSS_COLORS; if (array_key_exists($sosa_child, $sosa_points)) { // Would like to use a GeometryCollection to hold LineStrings // rather than generate polylines but the MarkerCluster library // doesn't seem to like them $polyline = [ 'points' => [ $sosa_points[$sosa_child], [$latitude, $longitude], ], 'options' => [ 'color' => $color, ], ]; } $geojson['features'][] = [ 'type' => 'Feature', 'id' => $sosa, 'geometry' => [ 'type' => 'Point', 'coordinates' => [$longitude, $latitude], ], 'properties' => [ 'polyline' => $polyline, 'iconcolor' => $color, 'tooltip' => null, 'summary' => view('modules/pedigree-map/events', [ 'class' => $class, 'fact' => $fact, 'relationship' => $this->getSosaName($sosa), 'sosa' => $sosa, ]), ], ]; } } return $geojson; } /** * @param ServerRequestInterface $request * @param ChartService $chart_service * * @return array */ protected function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array { $tree = Validator::attributes($request)->tree(); $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'); $xref = Validator::attributes($request)->isXref()->string('xref'); $individual = Registry::individualFactory()->make($xref, $tree); $individual = Auth::checkIndividualAccess($individual, false, true); $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); $facts = []; foreach ($ancestors as $sosa => $person) { if ($person->canShow()) { $birth = $person->facts(Gedcom::BIRTH_EVENTS, true) ->first(static fn (Fact $fact): bool => $fact->place()->gedcomName() !== ''); if ($birth instanceof Fact) { $facts[$sosa] = $birth; } } } return $facts; } /** * builds and returns sosa relationship name in the active language * * @param int $sosa Sosa number * * @return string */ protected function getSosaName(int $sosa): string { $path = ''; while ($sosa > 1) { if ($sosa % 2 === 1) { $path = 'mot' . $path; } else { $path = 'fat' . $path; } $sosa = intdiv($sosa, 2); } return ucfirst($this->relationship_service->legacyNameAlgorithm($path)); } }