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