1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Module; 19 20use Exception; 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 Fisharebest\Webtrees\Webtrees; 35use Psr\Http\Message\ResponseInterface; 36use Psr\Http\Message\ServerRequestInterface; 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 private static $map_providers; 72 private static $map_selections; 73 74 /** 75 * How should this module be identified in the control panel, etc.? 76 * 77 * @return string 78 */ 79 public function title(): string 80 { 81 /* I18N: Name of a module */ 82 return I18N::translate('Pedigree map'); 83 } 84 85 /** 86 * A sentence describing what this module does. 87 * 88 * @return string 89 */ 90 public function description(): string 91 { 92 /* I18N: Description of the “OSM” module */ 93 return I18N::translate('Show the birthplace of ancestors on a map.'); 94 } 95 96 /** 97 * CSS class for the URL. 98 * 99 * @return string 100 */ 101 public function chartMenuClass(): string 102 { 103 return 'menu-chart-pedigreemap'; 104 } 105 106 /** 107 * Return a menu item for this chart - for use in individual boxes. 108 * 109 * @param Individual $individual 110 * 111 * @return Menu|null 112 */ 113 public function chartBoxMenu(Individual $individual): ?Menu 114 { 115 return $this->chartMenu($individual); 116 } 117 118 /** 119 * The title for a specific instance of this chart. 120 * 121 * @param Individual $individual 122 * 123 * @return string 124 */ 125 public function chartTitle(Individual $individual): string 126 { 127 /* I18N: %s is an individual’s name */ 128 return I18N::translate('Pedigree map of %s', $individual->fullName()); 129 } 130 131 /** 132 * The URL for this chart. 133 * 134 * @param Individual $individual 135 * @param string[] $parameters 136 * 137 * @return string 138 */ 139 public function chartUrl(Individual $individual, array $parameters = []): string 140 { 141 return route('module', [ 142 'module' => $this->name(), 143 'action' => 'PedigreeMap', 144 'xref' => $individual->xref(), 145 'ged' => $individual->tree()->name(), 146 ] + $parameters); 147 } 148 149 /** 150 * @param ServerRequestInterface $request 151 * @param Tree $tree 152 * @param ChartService $chart_service 153 * 154 * @return ResponseInterface 155 */ 156 public function getMapDataAction(ServerRequestInterface $request, Tree $tree, ChartService $chart_service): ResponseInterface 157 { 158 $xref = $request->getQueryParams()['reference']; 159 $indi = Individual::getInstance($xref, $tree); 160 $color_count = count(self::LINE_COLORS); 161 162 $facts = $this->getPedigreeMapFacts($request, $tree, $chart_service); 163 164 $geojson = [ 165 'type' => 'FeatureCollection', 166 'features' => [], 167 ]; 168 169 $sosa_points = []; 170 171 foreach ($facts as $id => $fact) { 172 $location = new Location($fact->place()->gedcomName()); 173 174 // Use the co-ordinates from the fact (if they exist). 175 $latitude = $fact->latitude(); 176 $longitude = $fact->longitude(); 177 178 // Use the co-ordinates from the location otherwise. 179 if ($latitude === 0.0 && $longitude === 0.0) { 180 $latitude = $location->latitude(); 181 $longitude = $location->longitude(); 182 } 183 184 $icon = ['color' => 'Gold', 'name' => 'bullseye ']; 185 if ($latitude !== 0.0 || $longitude !== 0.0) { 186 $polyline = null; 187 $color = self::LINE_COLORS[log($id, 2) % $color_count]; 188 $icon['color'] = $color; //make icon color the same as the line 189 $sosa_points[$id] = [$latitude, $longitude]; 190 $sosa_parent = intdiv($id, 2); 191 if (array_key_exists($sosa_parent, $sosa_points)) { 192 // Would like to use a GeometryCollection to hold LineStrings 193 // rather than generate polylines but the MarkerCluster library 194 // doesn't seem to like them 195 $polyline = [ 196 'points' => [ 197 $sosa_points[$sosa_parent], 198 [$latitude, $longitude], 199 ], 200 'options' => [ 201 'color' => $color, 202 ], 203 ]; 204 } 205 $geojson['features'][] = [ 206 'type' => 'Feature', 207 'id' => $id, 208 'valid' => true, 209 'geometry' => [ 210 'type' => 'Point', 211 'coordinates' => [$longitude, $latitude], 212 ], 213 'properties' => [ 214 'polyline' => $polyline, 215 'icon' => $icon, 216 'tooltip' => strip_tags($fact->place()->fullName()), 217 'summary' => view('modules/pedigree-map/events', $this->summaryData($indi, $fact, $id)), 218 'zoom' => $location->zoom() ?: 2, 219 ], 220 ]; 221 } 222 } 223 224 $code = empty($facts) ? StatusCodeInterface::STATUS_NO_CONTENT : StatusCodeInterface::STATUS_OK; 225 226 return response($geojson, $code); 227 } 228 229 /** 230 * @param ServerRequestInterface $request 231 * @param Tree $tree 232 * @param ChartService $chart_service 233 * 234 * @return array 235 */ 236 private function getPedigreeMapFacts(ServerRequestInterface $request, Tree $tree, ChartService $chart_service): array 237 { 238 $xref = $request->getQueryParams()['reference']; 239 $individual = Individual::getInstance($xref, $tree); 240 $generations = (int) $request->getQueryParams()['generations']; 241 $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); 242 $facts = []; 243 foreach ($ancestors as $sosa => $person) { 244 if ($person->canShow()) { 245 $birth = $person->facts(['BIRT'])->first(); 246 if ($birth instanceof Fact && $birth->place()->gedcomName() !== '') { 247 $facts[$sosa] = $birth; 248 } 249 } 250 } 251 252 return $facts; 253 } 254 255 /** 256 * @param Individual $individual 257 * @param Fact $fact 258 * @param int $sosa 259 * 260 * @return array 261 */ 262 private function summaryData(Individual $individual, Fact $fact, int $sosa): array 263 { 264 $record = $fact->record(); 265 $name = ''; 266 $url = ''; 267 $tag = $fact->label(); 268 $addbirthtag = false; 269 270 if ($record instanceof Family) { 271 // Marriage 272 $spouse = $record->spouse($individual); 273 if ($spouse) { 274 $url = $spouse->url(); 275 $name = $spouse->fullName(); 276 } 277 } elseif ($record !== $individual) { 278 // Birth of a child 279 $url = $record->url(); 280 $name = $record->fullName(); 281 $tag = GedcomTag::getLabel('_BIRT_CHIL', $record); 282 } 283 284 if ($sosa > 1) { 285 $addbirthtag = true; 286 $tag = ucfirst($this->getSosaName($sosa)); 287 } 288 289 return [ 290 'tag' => $tag, 291 'url' => $url, 292 'name' => $name, 293 'value' => $fact->value(), 294 'date' => $fact->date()->display(true), 295 'place' => $fact->place(), 296 'addtag' => $addbirthtag, 297 ]; 298 } 299 300 /** 301 * @param ServerRequestInterface $request 302 * @param Tree $tree 303 * 304 * @return object 305 */ 306 public function getPedigreeMapAction(ServerRequestInterface $request, Tree $tree) 307 { 308 $xref = $request->getQueryParams()['xref']; 309 $individual = Individual::getInstance($xref, $tree); 310 $generations = $request->getQueryParams()['generations'] ?? self::DEFAULT_GENERATIONS; 311 312 if ($individual === null) { 313 throw new IndividualNotFoundException(); 314 } 315 316 if (!$individual->canShow()) { 317 throw new IndividualAccessDeniedException(); 318 } 319 320 return $this->viewResponse('modules/pedigree-map/page', [ 321 'module_name' => $this->name(), 322 /* I18N: %s is an individual’s name */ 323 'title' => I18N::translate('Pedigree map of %s', $individual->fullName()), 324 'tree' => $tree, 325 'individual' => $individual, 326 'generations' => $generations, 327 'maxgenerations' => self::MAXIMUM_GENERATIONS, 328 'map' => view( 329 'modules/pedigree-map/chart', 330 [ 331 'module' => $this->name(), 332 'ref' => $individual->xref(), 333 'type' => 'pedigree', 334 'generations' => $generations, 335 ] 336 ), 337 ]); 338 } 339 340 /** 341 * builds and returns sosa relationship name in the active language 342 * 343 * @param int $sosa Sosa number 344 * 345 * @return string 346 */ 347 private function getSosaName(int $sosa): string 348 { 349 $path = ''; 350 351 while ($sosa > 1) { 352 if ($sosa % 2 === 1) { 353 $path = 'mot' . $path; 354 } else { 355 $path = 'fat' . $path; 356 } 357 $sosa = intdiv($sosa, 2); 358 } 359 360 return Functions::getRelationshipNameFromPath($path); 361 } 362} 363