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