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