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