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