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