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