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