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\Fact; 24use Fisharebest\Webtrees\FactLocation; 25use Fisharebest\Webtrees\Functions\Functions; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Individual; 28use Fisharebest\Webtrees\View; 29use stdClass; 30use Symfony\Component\HttpFoundation\JsonResponse; 31use Symfony\Component\HttpFoundation\Request; 32 33/** 34 * Class PlacesMapModule 35 */ 36class PlacesModule extends AbstractModule implements ModuleTabInterface 37{ 38 private static $map_providers = null; 39 private static $map_selections = null; 40 41 /** {@inheritdoc} */ 42 public function getTitle() 43 { 44 return /* I18N: Name of a module */ 45 I18N::translate('Places'); 46 } 47 48 /** {@inheritdoc} */ 49 public function getDescription() 50 { 51 return /* I18N: Description of the “OSM” module */ 52 I18N::translate('Show the location of events on a map.'); 53 } 54 55 /** {@inheritdoc} */ 56 public function defaultAccessLevel() 57 { 58 return Auth::PRIV_PRIVATE; 59 } 60 61 /** {@inheritdoc} */ 62 public function defaultTabOrder() 63 { 64 return 4; 65 } 66 67 /** {@inheritdoc} */ 68 public function hasTabContent(Individual $individual) 69 { 70 return true; 71 } 72 73 /** {@inheritdoc} */ 74 public function isGrayedOut(Individual $individual) 75 { 76 return false; 77 } 78 79 /** {@inheritdoc} */ 80 public function canLoadAjax() 81 { 82 return true; 83 } 84 85 /** {@inheritdoc} */ 86 public function getTabContent(Individual $individual) 87 { 88 return view('modules/places/tab', [ 89 'data' => $this->getMapData($individual), 90 ]); 91 } 92 93 /** 94 * @param Request $request 95 * 96 * @return stdClass 97 */ 98 private function getMapData(Individual $indi): stdClass 99 { 100 $facts = $this->getPersonalFacts($indi); 101 102 $geojson = [ 103 'type' => 'FeatureCollection', 104 'features' => [], 105 ]; 106 107 foreach ($facts as $id => $fact) { 108 $event = new FactLocation($fact, $indi); 109 $icon = $event->getIconDetails(); 110 if ($event->knownLatLon()) { 111 $geojson['features'][] = [ 112 'type' => 'Feature', 113 'id' => $id, 114 'valid' => true, 115 'geometry' => [ 116 'type' => 'Point', 117 'coordinates' => $event->getGeoJsonCoords(), 118 ], 119 'properties' => [ 120 'polyline' => null, 121 'icon' => $icon, 122 'tooltip' => $event->toolTip(), 123 'summary' => view( 124 'modules/places/event-sidebar', 125 $event->shortSummary('individual', $id) 126 ), 127 'zoom' => (int) $event->getZoom(), 128 ], 129 ]; 130 } 131 } 132 133 return (object) $geojson; 134 } 135 136 /** 137 * @param Individual $individual 138 * 139 * @return array 140 * @throws Exception 141 */ 142 private function getPersonalFacts(Individual $individual) 143 { 144 $facts = $individual->getFacts(); 145 foreach ($individual->getSpouseFamilies() as $family) { 146 $facts = array_merge($facts, $family->getFacts()); 147 // Add birth of children from this family to the facts array 148 foreach ($family->getChildren() as $child) { 149 $childsBirth = $child->getFirstFact('BIRT'); 150 if ($childsBirth && !$childsBirth->getPlace()->isEmpty()) { 151 $facts[] = $childsBirth; 152 } 153 } 154 } 155 156 Functions::sortFacts($facts); 157 158 $useable_facts = array_filter( 159 $facts, 160 function (Fact $item) { 161 return !$item->getPlace()->isEmpty(); 162 } 163 ); 164 165 return array_values($useable_facts); 166 } 167 168 /** 169 * @param Request $request 170 * 171 * @return JsonResponse 172 */ 173 public function getProviderStylesAction(Request $request): JsonResponse 174 { 175 $styles = $this->getMapProviderData($request); 176 177 return new JsonResponse($styles); 178 } 179 180 /** 181 * @param Request $request 182 * 183 * @return array|null 184 */ 185 private function getMapProviderData(Request $request) 186 { 187 if (self::$map_providers === null) { 188 $providersFile = WT_ROOT . WT_MODULES_DIR . 'openstreetmap/providers/providers.xml'; 189 self::$map_selections = [ 190 'provider' => $this->getPreference('provider', 'openstreetmap'), 191 'style' => $this->getPreference('provider_style', 'mapnik'), 192 ]; 193 194 try { 195 $xml = simplexml_load_file($providersFile); 196 // need to convert xml structure into arrays & strings 197 foreach ($xml as $provider) { 198 $style_keys = array_map( 199 function ($item) { 200 return preg_replace('/[^a-z\d]/i', '', strtolower($item)); 201 }, 202 (array) $provider->styles 203 ); 204 205 $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name)); 206 207 self::$map_providers[$key] = [ 208 'name' => (string) $provider->name, 209 'styles' => array_combine($style_keys, (array) $provider->styles), 210 ]; 211 } 212 } catch (Exception $ex) { 213 // Default provider is OpenStreetMap 214 self::$map_selections = [ 215 'provider' => 'openstreetmap', 216 'style' => 'mapnik', 217 ]; 218 self::$map_providers = [ 219 'openstreetmap' => [ 220 'name' => 'OpenStreetMap', 221 'styles' => ['mapnik' => 'Mapnik'], 222 ], 223 ]; 224 }; 225 } 226 227 //Ugly!!! 228 switch ($request->get('action')) { 229 case 'BaseData': 230 $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']]; 231 $payload = [ 232 'selectedProvIndex' => self::$map_selections['provider'], 233 'selectedProvName' => self::$map_providers[self::$map_selections['provider']]['name'], 234 'selectedStyleName' => $varName, 235 ]; 236 break; 237 case 'ProviderStyles': 238 $provider = $request->get('provider', 'openstreetmap'); 239 $payload = self::$map_providers[$provider]['styles']; 240 break; 241 case 'AdminConfig': 242 $providers = []; 243 foreach (self::$map_providers as $key => $provider) { 244 $providers[$key] = $provider['name']; 245 } 246 $payload = [ 247 'providers' => $providers, 248 'selectedProv' => self::$map_selections['provider'], 249 'styles' => self::$map_providers[self::$map_selections['provider']]['styles'], 250 'selectedStyle' => self::$map_selections['style'], 251 ]; 252 break; 253 default: 254 $payload = null; 255 } 256 257 return $payload; 258 } 259 260 /** 261 * @param $parent_id 262 * @param $placename 263 * @param $places 264 * 265 * @throws Exception 266 */ 267 private function buildLevel($parent_id, $placename, &$places) 268 { 269 $level = array_search('', $placename); 270 $rows = (array) Database::prepare( 271 "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" 272 ) 273 ->execute([$parent_id]) 274 ->fetchAll(\PDO::FETCH_ASSOC); 275 276 if (!empty($rows)) { 277 foreach ($rows as $row) { 278 $index = $row['pl_id']; 279 $placename[$level] = $row['pl_place']; 280 $places[] = array_merge([$row['pl_level']], $placename, array_splice($row, 3)); 281 $this->buildLevel($index, $placename, $places); 282 } 283 } 284 } 285} 286