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