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 Fisharebest\Webtrees\Exceptions\IndividualAccessDeniedException; 22use Fisharebest\Webtrees\Exceptions\IndividualNotFoundException; 23use Fisharebest\Webtrees\Fact; 24use Fisharebest\Webtrees\FactLocation; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Menu; 28use Fisharebest\Webtrees\Services\ChartService; 29use Fisharebest\Webtrees\Tree; 30use Fisharebest\Webtrees\Webtrees; 31use Symfony\Component\HttpFoundation\JsonResponse; 32use Symfony\Component\HttpFoundation\Request; 33use Symfony\Component\HttpFoundation\Response; 34 35/** 36 * Class PedigreeMapModule 37 */ 38class PedigreeMapModule extends AbstractModule implements ModuleChartInterface 39{ 40 use ModuleChartTrait; 41 42 private const LINE_COLORS = [ 43 '#FF0000', 44 // Red 45 '#00FF00', 46 // Green 47 '#0000FF', 48 // Blue 49 '#FFB300', 50 // Gold 51 '#00FFFF', 52 // Cyan 53 '#FF00FF', 54 // Purple 55 '#7777FF', 56 // Light blue 57 '#80FF80' 58 // Light green 59 ]; 60 61 private static $map_providers = null; 62 private static $map_selections = null; 63 64 /** 65 * How should this module be labelled on tabs, menus, etc.? 66 * 67 * @return string 68 */ 69 public function title(): string 70 { 71 /* I18N: Name of a module */ 72 return I18N::translate('Pedigree map'); 73 } 74 75 /** 76 * A sentence describing what this module does. 77 * 78 * @return string 79 */ 80 public function description(): string 81 { 82 /* I18N: Description of the “OSM” module */ 83 return I18N::translate('Show the birthplace of ancestors on a map.'); 84 } 85 86 /** 87 * CSS class for the URL. 88 * 89 * @return string 90 */ 91 public function chartMenuClass(): string 92 { 93 return 'menu-chart-pedigreemap'; 94 } 95 96 /** 97 * Return a menu item for this chart - for use in individual boxes. 98 * 99 * @param Individual $individual 100 * 101 * @return Menu|null 102 */ 103 public function chartBoxMenu(Individual $individual): ?Menu 104 { 105 return $this->chartMenu($individual); 106 } 107 108 /** 109 * The title for a specific instance of this chart. 110 * 111 * @param Individual $individual 112 * 113 * @return string 114 */ 115 public function chartTitle(Individual $individual): string 116 { 117 /* I18N: %s is an individual’s name */ 118 return I18N::translate('Pedigree map of %s', $individual->getFullName()); 119 } 120 121 /** 122 * The URL for this chart. 123 * 124 * @param Individual $individual 125 * @param string[] $parameters 126 * 127 * @return string 128 */ 129 public function chartUrl(Individual $individual, array $parameters = []): string 130 { 131 return route('module', [ 132 'module' => $this->name(), 133 'action' => 'PedigreeMap', 134 'xref' => $individual->xref(), 135 'ged' => $individual->tree()->name(), 136 ] + $parameters); 137 } 138 139 /** 140 * @param Request $request 141 * @param Tree $tree 142 * @param ChartService $chart_service 143 * 144 * @return JsonResponse 145 */ 146 public function getMapDataAction(Request $request, Tree $tree, ChartService $chart_service): JsonResponse 147 { 148 $xref = $request->get('reference'); 149 $indi = Individual::getInstance($xref, $tree); 150 $color_count = count(self::LINE_COLORS); 151 152 $facts = $this->getPedigreeMapFacts($request, $tree, $chart_service); 153 154 $geojson = [ 155 'type' => 'FeatureCollection', 156 'features' => [], 157 ]; 158 159 $sosa_points = []; 160 161 foreach ($facts as $id => $fact) { 162 $event = new FactLocation($fact, $indi); 163 $icon = $event->getIconDetails(); 164 if ($event->knownLatLon()) { 165 $polyline = null; 166 $color = self::LINE_COLORS[log($id, 2) % $color_count]; 167 $icon['color'] = $color; //make icon color the same as the line 168 $sosa_points[$id] = $event->getLatLonJSArray(); 169 $sosa_parent = intdiv($id, 2); 170 if (array_key_exists($sosa_parent, $sosa_points)) { 171 // Would like to use a GeometryCollection to hold LineStrings 172 // rather than generate polylines but the MarkerCluster library 173 // doesn't seem to like them 174 $polyline = [ 175 'points' => [ 176 $sosa_points[$sosa_parent], 177 $event->getLatLonJSArray(), 178 ], 179 'options' => [ 180 'color' => $color, 181 ], 182 ]; 183 } 184 $geojson['features'][] = [ 185 'type' => 'Feature', 186 'id' => $id, 187 'valid' => true, 188 'geometry' => [ 189 'type' => 'Point', 190 'coordinates' => $event->getGeoJsonCoords(), 191 ], 192 'properties' => [ 193 'polyline' => $polyline, 194 'icon' => $icon, 195 'tooltip' => $event->toolTip(), 196 'summary' => view('modules/pedigree-map/events', $event->shortSummary('pedigree', $id)), 197 'zoom' => (int) $event->getZoom(), 198 ], 199 ]; 200 } 201 } 202 203 $code = empty($facts) ? Response::HTTP_NO_CONTENT : Response::HTTP_OK; 204 205 return new JsonResponse($geojson, $code); 206 } 207 208 /** 209 * @param Request $request 210 * @param Tree $tree 211 * @param ChartService $chart_service 212 * 213 * @return array 214 */ 215 private function getPedigreeMapFacts(Request $request, Tree $tree, ChartService $chart_service): array 216 { 217 $xref = $request->get('reference'); 218 $individual = Individual::getInstance($xref, $tree); 219 $generations = (int) $request->get( 220 'generations', 221 $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS') 222 ); 223 $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); 224 $facts = []; 225 foreach ($ancestors as $sosa => $person) { 226 if ($person->canShow()) { 227 $birth = $person->getFirstFact('BIRT'); 228 if ($birth instanceof Fact && !$birth->place()->isEmpty()) { 229 $facts[$sosa] = $birth; 230 } 231 } 232 } 233 234 return $facts; 235 } 236 237 /** 238 * @param Request $request 239 * 240 * @return JsonResponse 241 */ 242 public function getProviderStylesAction(Request $request): JsonResponse 243 { 244 $styles = $this->getMapProviderData($request); 245 246 return new JsonResponse($styles); 247 } 248 249 /** 250 * @param Request $request 251 * 252 * @return array|null 253 */ 254 private function getMapProviderData(Request $request) 255 { 256 if (self::$map_providers === null) { 257 $providersFile = WT_ROOT . Webtrees::MODULES_PATH . 'openstreetmap/providers/providers.xml'; 258 self::$map_selections = [ 259 'provider' => $this->getPreference('provider', 'openstreetmap'), 260 'style' => $this->getPreference('provider_style', 'mapnik'), 261 ]; 262 263 try { 264 $xml = simplexml_load_file($providersFile); 265 // need to convert xml structure into arrays & strings 266 foreach ($xml as $provider) { 267 $style_keys = array_map( 268 function (string $item): string { 269 return preg_replace('/[^a-z\d]/i', '', strtolower($item)); 270 }, 271 (array) $provider->styles 272 ); 273 274 $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name)); 275 276 self::$map_providers[$key] = [ 277 'name' => (string) $provider->name, 278 'styles' => array_combine($style_keys, (array) $provider->styles), 279 ]; 280 } 281 } catch (Exception $ex) { 282 // Default provider is OpenStreetMap 283 self::$map_selections = [ 284 'provider' => 'openstreetmap', 285 'style' => 'mapnik', 286 ]; 287 self::$map_providers = [ 288 'openstreetmap' => [ 289 'name' => 'OpenStreetMap', 290 'styles' => ['mapnik' => 'Mapnik'], 291 ], 292 ]; 293 }; 294 } 295 296 //Ugly!!! 297 switch ($request->get('action')) { 298 case 'BaseData': 299 $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']]; 300 $payload = [ 301 'selectedProvIndex' => self::$map_selections['provider'], 302 'selectedProvName' => self::$map_providers[self::$map_selections['provider']]['name'], 303 'selectedStyleName' => $varName, 304 ]; 305 break; 306 case 'ProviderStyles': 307 $provider = $request->get('provider', 'openstreetmap'); 308 $payload = self::$map_providers[$provider]['styles']; 309 break; 310 case 'AdminConfig': 311 $providers = []; 312 foreach (self::$map_providers as $key => $provider) { 313 $providers[$key] = $provider['name']; 314 } 315 $payload = [ 316 'providers' => $providers, 317 'selectedProv' => self::$map_selections['provider'], 318 'styles' => self::$map_providers[self::$map_selections['provider']]['styles'], 319 'selectedStyle' => self::$map_selections['style'], 320 ]; 321 break; 322 default: 323 $payload = null; 324 } 325 326 return $payload; 327 } 328 329 /** 330 * @param Request $request 331 * @param Tree $tree 332 * 333 * @return object 334 */ 335 public function getPedigreeMapAction(Request $request, Tree $tree) 336 { 337 $xref = $request->get('xref', ''); 338 $individual = Individual::getInstance($xref, $tree); 339 $maxgenerations = $tree->getPreference('MAX_PEDIGREE_GENERATIONS'); 340 $generations = $request->get('generations', $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS')); 341 342 if ($individual === null) { 343 throw new IndividualNotFoundException(); 344 } 345 346 if (!$individual->canShow()) { 347 throw new IndividualAccessDeniedException(); 348 } 349 350 return $this->viewResponse('modules/pedigree-map/page', [ 351 'module_name' => $this->name(), 352 /* I18N: %s is an individual’s name */ 353 'title' => I18N::translate('Pedigree map of %s', $individual->getFullName()), 354 'tree' => $tree, 355 'individual' => $individual, 356 'generations' => $generations, 357 'maxgenerations' => $maxgenerations, 358 'map' => view( 359 'modules/pedigree-map/chart', 360 [ 361 'module' => $this->name(), 362 'ref' => $individual->xref(), 363 'type' => 'pedigree', 364 'generations' => $generations, 365 ] 366 ), 367 ]); 368 } 369} 370