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