1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 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\Auth; 22use Fisharebest\Webtrees\Database; 23use Fisharebest\Webtrees\DebugBar; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\FactLocation; 26use Fisharebest\Webtrees\FlashMessages; 27use Fisharebest\Webtrees\Functions\Functions; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Individual; 30use Fisharebest\Webtrees\Location; 31use Fisharebest\Webtrees\Log; 32use Fisharebest\Webtrees\Menu; 33use Fisharebest\Webtrees\Place; 34use Fisharebest\Webtrees\Stats; 35use Fisharebest\Webtrees\Tree; 36use Symfony\Component\HttpFoundation\JsonResponse; 37use Symfony\Component\HttpFoundation\RedirectResponse; 38use Symfony\Component\HttpFoundation\Request; 39 40/** 41 * Class PlacesMapModule 42 */ 43class PlacesModule extends AbstractModule implements ModuleTabInterface 44{ 45 const OSM_MIN_ZOOM = 2; 46 const LINE_COLORS = [ 47 '#FF0000', 48 // Red 49 '#00FF00', 50 // Green 51 '#0000FF', 52 // Blue 53 '#FFB300', 54 // Gold 55 '#00FFFF', 56 // Cyan 57 '#FF00FF', 58 // Purple 59 '#7777FF', 60 // Light blue 61 '#80FF80' 62 // Light green 63 ]; 64 65 private static $map_providers = null; 66 private static $map_selections = null; 67 68 /** {@inheritdoc} */ 69 public function getTitle() 70 { 71 return /* I18N: Name of a module */ 72 I18N::translate('Places'); 73 } 74 75 /** {@inheritdoc} */ 76 public function getDescription() 77 { 78 return /* I18N: Description of the “OSM” module */ 79 I18N::translate('Show the location of events on a map.'); 80 } 81 82 /** {@inheritdoc} */ 83 public function defaultAccessLevel() 84 { 85 return Auth::PRIV_PRIVATE; 86 } 87 88 /** {@inheritdoc} */ 89 public function defaultTabOrder() 90 { 91 return 4; 92 } 93 94 /** {@inheritdoc} */ 95 public function hasTabContent(Individual $individual) 96 { 97 return true; 98 } 99 100 /** {@inheritdoc} */ 101 public function isGrayedOut(Individual $individual) 102 { 103 return false; 104 } 105 106 /** {@inheritdoc} */ 107 public function canLoadAjax() 108 { 109 return true; 110 } 111 112 /** 113 * @param string $type 114 * 115 * @return array 116 */ 117 public function assets($type = 'user') 118 { 119 $dir = WT_MODULES_DIR . $this->getName(); 120 if ($type === 'admin') { 121 return [ 122 'css' => [ 123 $dir . '/assets/css/osm-module.css', 124 ], 125 'js' => [ 126 $dir . '/assets/js/osm-admin.js', 127 ], 128 ]; 129 } else { 130 return [ 131 'css' => [ 132 $dir . '/assets/css/osm-module.css', 133 ], 134 'js' => [ 135 $dir . '/assets/js/osm-module.js', 136 ], 137 ]; 138 } 139 } 140 141 /** {@inheritdoc} */ 142 public function getTabContent(Individual $individual) 143 { 144 145 return view( 146 'modules/openstreetmap/map', 147 [ 148 'assets' => $this->assets(), 149 'module' => $this->getName(), 150 'ref' => $individual->getXref(), 151 'type' => 'individual', 152 ] 153 ); 154 } 155 156 /** 157 * @param Request $request 158 * 159 * @return JsonResponse 160 */ 161 public function getBaseDataAction(Request $request): JsonResponse 162 { 163 $provider = $this->getMapProviderData($request); 164 $style = $provider['selectedStyleName'] = '' ? '' : '.' . $provider['selectedStyleName']; 165 166 switch ($provider['selectedProvIndex']) { 167 case 'mapbox': 168 $providerOptions = [ 169 'id' => $this->getPreference('mapbox_id'), 170 'accessToken' => $this->getPreference('mapbox_token'), 171 ]; 172 break; 173 case 'here': 174 $providerOptions = [ 175 'app_id' => $this->getPreference('here_appid'), 176 'app_code' => $this->getPreference('here_appcode'), 177 ]; 178 break; 179 default: 180 $providerOptions = []; 181 }; 182 183 $options = [ 184 'minZoom' => self::OSM_MIN_ZOOM, 185 'providerName' => $provider['selectedProvName'] . $style, 186 'providerOptions' => $providerOptions, 187 'animate' => $this->getPreference('map_animate', 0), 188 'I18N' => [ 189 'zoomInTitle' => I18N::translate('Zoom in'), 190 'zoomOutTitle' => I18N::translate('Zoom out'), 191 'reset' => I18N::translate('Reset to initial map state'), 192 'noData' => I18N::translate('No mappable items'), 193 'error' => I18N::translate('An unknown error occurred'), 194 ], 195 ]; 196 197 return new JsonResponse($options); 198 } 199 200 /** 201 * @param Request $request 202 * 203 * @return JsonResponse 204 * @throws Exception 205 */ 206 public function getMapDataAction(Request $request): JsonResponse 207 { 208 $xref = $request->get('reference'); 209 $tree = $request->attributes->get('tree'); 210 $indi = Individual::getInstance($xref, $tree); 211 212 $facts = $this->getPersonalFacts($request); 213 214 $geojson = [ 215 'type' => 'FeatureCollection', 216 'features' => [], 217 ]; 218 if (empty($facts)) { 219 $code = 204; 220 } else { 221 $code = 200; 222 foreach ($facts as $id => $fact) { 223 $event = new FactLocation($fact, $indi); 224 $icon = $event->getIconDetails(); 225 if ($event->knownLatLon()) { 226 $polyline = null; 227 $geojson['features'][] = [ 228 'type' => 'Feature', 229 'id' => $id, 230 'valid' => true, 231 'geometry' => [ 232 'type' => 'Point', 233 'coordinates' => $event->getGeoJsonCoords(), 234 ], 235 'properties' => [ 236 'polyline' => $polyline, 237 'icon' => $icon, 238 'tooltip' => $event->toolTip(), 239 'summary' => view( 240 'modules/openstreetmap/event-sidebar', 241 $event->shortSummary('individual', $id) 242 ), 243 'zoom' => (int)$event->getZoom(), 244 ], 245 ]; 246 } 247 } 248 } 249 250 return new JsonResponse($geojson, $code); 251 } 252 253 /** 254 * @param Request $request 255 * 256 * @return array 257 * @throws Exception 258 */ 259 private function getPersonalFacts(Request $request) 260 { 261 $xref = $request->get('reference'); 262 $tree = $request->attributes->get('tree'); 263 $individual = Individual::getInstance($xref, $tree); 264 $facts = $individual->getFacts(); 265 foreach ($individual->getSpouseFamilies() as $family) { 266 $facts = array_merge($facts, $family->getFacts()); 267 // Add birth of children from this family to the facts array 268 foreach ($family->getChildren() as $child) { 269 $childsBirth = $child->getFirstFact('BIRT'); 270 if ($childsBirth && !$childsBirth->getPlace()->isEmpty()) { 271 $facts[] = $childsBirth; 272 } 273 } 274 } 275 276 Functions::sortFacts($facts); 277 278 $useable_facts = array_filter( 279 $facts, 280 function (Fact $item) { 281 return !$item->getPlace()->isEmpty(); 282 } 283 ); 284 285 return array_values($useable_facts); 286 } 287 288 /** 289 * @param Request $request 290 * 291 * @return JsonResponse 292 */ 293 public function getProviderStylesAction(Request $request): JsonResponse 294 { 295 $styles = $this->getMapProviderData($request); 296 297 return new JsonResponse($styles); 298 } 299 300 /** 301 * @param Request $request 302 * 303 * @return array|null 304 */ 305 private function getMapProviderData(Request $request) 306 { 307 if (self::$map_providers === null) { 308 $providersFile = WT_ROOT . WT_MODULES_DIR . $this->getName() . '/providers/providers.xml'; 309 self::$map_selections = [ 310 'provider' => $this->getPreference('provider', 'openstreetmap'), 311 'style' => $this->getPreference('provider_style', 'mapnik'), 312 ]; 313 314 try { 315 $xml = simplexml_load_file($providersFile); 316 // need to convert xml structure into arrays & strings 317 foreach ($xml as $provider) { 318 $style_keys = array_map( 319 function ($item) { 320 return preg_replace('/[^a-z\d]/i', '', strtolower($item)); 321 }, 322 (array)$provider->styles 323 ); 324 325 $key = preg_replace('/[^a-z\d]/i', '', strtolower((string)$provider->name)); 326 327 self::$map_providers[$key] = [ 328 'name' => (string)$provider->name, 329 'styles' => array_combine($style_keys, (array)$provider->styles), 330 ]; 331 } 332 } catch (Exception $ex) { 333 // Default provider is OpenStreetMap 334 self::$map_selections = [ 335 'provider' => 'openstreetmap', 336 'style' => 'mapnik', 337 ]; 338 self::$map_providers = [ 339 'openstreetmap' => [ 340 'name' => 'OpenStreetMap', 341 'styles' => ['mapnik' => 'Mapnik'], 342 ], 343 ]; 344 }; 345 } 346 347 //Ugly!!! 348 switch ($request->get('action')) { 349 case 'BaseData': 350 $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']]; 351 $payload = [ 352 'selectedProvIndex' => self::$map_selections['provider'], 353 'selectedProvName' => self::$map_providers[self::$map_selections['provider']]['name'], 354 'selectedStyleName' => $varName, 355 ]; 356 break; 357 case 'ProviderStyles': 358 $provider = $request->get('provider', 'openstreetmap'); 359 $payload = self::$map_providers[$provider]['styles']; 360 break; 361 case 'AdminConfig': 362 $providers = []; 363 foreach (self::$map_providers as $key => $provider) { 364 $providers[$key] = $provider['name']; 365 } 366 $payload = [ 367 'providers' => $providers, 368 'selectedProv' => self::$map_selections['provider'], 369 'styles' => self::$map_providers[self::$map_selections['provider']]['styles'], 370 'selectedStyle' => self::$map_selections['style'], 371 ]; 372 break; 373 default: 374 $payload = null; 375 } 376 377 return $payload; 378 } 379 380 /** 381 * @param $parent_id 382 * @param $placename 383 * @param $places 384 * 385 * @throws Exception 386 */ 387 private function buildLevel($parent_id, $placename, &$places) 388 { 389 $level = array_search('', $placename); 390 $rows = (array)Database::prepare( 391 "SELECT pl_level, pl_id, pl_place, pl_long, pl_lati, pl_zoom, pl_icon FROM `##placelocation` WHERE pl_parent_id=? ORDER BY pl_place" 392 ) 393 ->execute([$parent_id]) 394 ->fetchAll(\PDO::FETCH_ASSOC); 395 396 if (!empty($rows)) { 397 foreach ($rows as $row) { 398 $index = $row['pl_id']; 399 $placename[$level] = $row['pl_place']; 400 $places[] = array_merge([$row['pl_level']], $placename, array_splice($row, 3)); 401 $this->buildLevel($index, $placename, $places); 402 } 403 } 404 } 405} 406