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\Family; 23use Fisharebest\Webtrees\GedcomTag; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Individual; 26use Fisharebest\Webtrees\Location; 27use Fisharebest\Webtrees\Site; 28use Fisharebest\Webtrees\Webtrees; 29use Illuminate\Support\Collection; 30use Psr\Http\Message\ResponseInterface; 31use Psr\Http\Message\ServerRequestInterface; 32use stdClass; 33 34/** 35 * Class PlacesMapModule 36 */ 37class PlacesModule extends AbstractModule implements ModuleTabInterface 38{ 39 use ModuleTabTrait; 40 41 private static $map_providers = null; 42 private static $map_selections = null; 43 44 public const ICONS = [ 45 'BIRT' => ['color' => 'Crimson', 'name' => 'birthday-cake'], 46 'MARR' => ['color' => 'Green', 'name' => 'venus-mars'], 47 'DEAT' => ['color' => 'Black', 'name' => 'plus'], 48 'CENS' => ['color' => 'MediumBlue', 'name' => 'users'], 49 'RESI' => ['color' => 'MediumBlue', 'name' => 'home'], 50 'OCCU' => ['color' => 'MediumBlue', 'name' => 'briefcase'], 51 'GRAD' => ['color' => 'MediumBlue', 'name' => 'graduation-cap'], 52 'EDUC' => ['color' => 'MediumBlue', 'name' => 'university'], 53 ]; 54 55 public const DEFAULT_ICON = ['color' => 'Gold', 'name' => 'bullseye ']; 56 57 /** 58 * How should this module be identified in the control panel, etc.? 59 * 60 * @return string 61 */ 62 public function title(): string 63 { 64 /* I18N: Name of a module */ 65 return I18N::translate('Places'); 66 } 67 68 /** 69 * A sentence describing what this module does. 70 * 71 * @return string 72 */ 73 public function description(): string 74 { 75 /* I18N: Description of the “OSM” module */ 76 return I18N::translate('Show the location of events on a map.'); 77 } 78 79 /** 80 * The default position for this tab. It can be changed in the control panel. 81 * 82 * @return int 83 */ 84 public function defaultTabOrder(): int 85 { 86 return 8; 87 } 88 89 /** {@inheritdoc} */ 90 public function hasTabContent(Individual $individual): bool 91 { 92 return Site::getPreference('map-provider') !== ''; 93 } 94 95 /** {@inheritdoc} */ 96 public function isGrayedOut(Individual $individual): bool 97 { 98 return false; 99 } 100 101 /** {@inheritdoc} */ 102 public function canLoadAjax(): bool 103 { 104 return true; 105 } 106 107 /** {@inheritdoc} */ 108 public function getTabContent(Individual $individual): string 109 { 110 return view('modules/places/tab', [ 111 'data' => $this->getMapData($individual), 112 ]); 113 } 114 115 /** 116 * @param Individual $indi 117 * 118 * @return stdClass 119 */ 120 private function getMapData(Individual $indi): stdClass 121 { 122 $facts = $this->getPersonalFacts($indi); 123 124 $geojson = [ 125 'type' => 'FeatureCollection', 126 'features' => [], 127 ]; 128 129 foreach ($facts as $id => $fact) { 130 $location = new Location($fact->place()->gedcomName()); 131 132 // Use the co-ordinates from the fact (if they exist). 133 $latitude = $fact->latitude(); 134 $longitude = $fact->longitude(); 135 136 // Use the co-ordinates from the location otherwise. 137 if ($latitude === 0.0 && $longitude === 0.0) { 138 $latitude = $location->latitude(); 139 $longitude = $location->longitude(); 140 } 141 142 $icon = self::ICONS[$fact->getTag()] ?? self::DEFAULT_ICON; 143 144 if ($latitude !== 0.0 || $longitude !== 0.0) { 145 $geojson['features'][] = [ 146 'type' => 'Feature', 147 'id' => $id, 148 'valid' => true, 149 'geometry' => [ 150 'type' => 'Point', 151 'coordinates' => [$longitude, $latitude], 152 ], 153 'properties' => [ 154 'polyline' => null, 155 'icon' => $icon, 156 'tooltip' => strip_tags($fact->place()->fullName()), 157 'summary' => view('modules/places/event-sidebar', $this->summaryData($indi, $fact)), 158 'zoom' => $location->zoom(), 159 ], 160 ]; 161 } 162 } 163 164 return (object) $geojson; 165 } 166 167 /** 168 * @param Individual $individual 169 * @param Fact $fact 170 * 171 * @return mixed[] 172 */ 173 private function summaryData(Individual $individual, Fact $fact): array 174 { 175 $record = $fact->record(); 176 $name = ''; 177 $url = ''; 178 $tag = $fact->label(); 179 180 if ($record instanceof Family) { 181 // Marriage 182 $spouse = $record->spouse($individual); 183 if ($spouse instanceof Individual) { 184 $url = $spouse->url(); 185 $name = $spouse->fullName(); 186 } 187 } elseif ($record !== $individual) { 188 // Birth of a child 189 $url = $record->url(); 190 $name = $record->fullName(); 191 $tag = GedcomTag::getLabel('_BIRT_CHIL', $record); 192 } 193 194 return [ 195 'tag' => $tag, 196 'url' => $url, 197 'name' => $name, 198 'value' => $fact->value(), 199 'date' => $fact->date()->display(true), 200 'place' => $fact->place(), 201 'addtag' => false, 202 ]; 203 } 204 205 /** 206 * @param Individual $individual 207 * 208 * @return Collection 209 * @return Fact[] 210 * @throws Exception 211 */ 212 private function getPersonalFacts(Individual $individual): Collection 213 { 214 $facts = $individual->facts(); 215 216 foreach ($individual->spouseFamilies() as $family) { 217 $facts = $facts->merge($family->facts()); 218 // Add birth of children from this family to the facts array 219 foreach ($family->children() as $child) { 220 $childsBirth = $child->facts(['BIRT'])->first(); 221 if ($childsBirth && $childsBirth->place()->gedcomName() !== '') { 222 $facts->push($childsBirth); 223 } 224 } 225 } 226 227 $facts = Fact::sortFacts($facts); 228 229 return $facts->filter(static function (Fact $item): bool { 230 return $item->place()->gedcomName() !== ''; 231 }); 232 } 233 234 /** 235 * @param ServerRequestInterface $request 236 * 237 * @return ResponseInterface 238 */ 239 public function getProviderStylesAction(ServerRequestInterface $request): ResponseInterface 240 { 241 $styles = $this->getMapProviderData($request); 242 243 return response($styles); 244 } 245 246 /** 247 * @param ServerRequestInterface $request 248 * 249 * @return array|null 250 */ 251 private function getMapProviderData(ServerRequestInterface $request): ?array 252 { 253 if (self::$map_providers === null) { 254 $providersFile = Webtrees::MODULES_DIR . 'openstreetmap/providers/providers.xml'; 255 self::$map_selections = [ 256 'provider' => $this->getPreference('provider', 'openstreetmap'), 257 'style' => $this->getPreference('provider_style', 'mapnik'), 258 ]; 259 260 try { 261 $xml = simplexml_load_string(file_get_contents($providersFile)); 262 // need to convert xml structure into arrays & strings 263 foreach ($xml as $provider) { 264 $style_keys = array_map( 265 static function (string $item): string { 266 return preg_replace('/[^a-z\d]/i', '', strtolower($item)); 267 }, 268 (array) $provider->styles 269 ); 270 271 $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name)); 272 273 self::$map_providers[$key] = [ 274 'name' => (string) $provider->name, 275 'styles' => array_combine($style_keys, (array) $provider->styles), 276 ]; 277 } 278 } catch (Exception $ex) { 279 // Default provider is OpenStreetMap 280 self::$map_selections = [ 281 'provider' => 'openstreetmap', 282 'style' => 'mapnik', 283 ]; 284 self::$map_providers = [ 285 'openstreetmap' => [ 286 'name' => 'OpenStreetMap', 287 'styles' => ['mapnik' => 'Mapnik'], 288 ], 289 ]; 290 } 291 } 292 293 //Ugly!!! 294 switch ($request->get('action')) { 295 case 'BaseData': 296 $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']]; 297 $payload = [ 298 'selectedProvIndex' => self::$map_selections['provider'], 299 'selectedProvName' => self::$map_providers[self::$map_selections['provider']]['name'], 300 'selectedStyleName' => $varName, 301 ]; 302 break; 303 case 'ProviderStyles': 304 $provider = $request->get('provider', 'openstreetmap'); 305 $payload = self::$map_providers[$provider]['styles']; 306 break; 307 case 'AdminConfig': 308 $providers = []; 309 foreach (self::$map_providers as $key => $provider) { 310 $providers[$key] = $provider['name']; 311 } 312 $payload = [ 313 'providers' => $providers, 314 'selectedProv' => self::$map_selections['provider'], 315 'styles' => self::$map_providers[self::$map_selections['provider']]['styles'], 316 'selectedStyle' => self::$map_selections['style'], 317 ]; 318 break; 319 default: 320 $payload = null; 321 } 322 323 return $payload; 324 } 325} 326