11f374598SGreg Roach<?php 21f374598SGreg Roach/** 31f374598SGreg Roach * webtrees: online genealogy 48fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team 51f374598SGreg Roach * This program is free software: you can redistribute it and/or modify 61f374598SGreg Roach * it under the terms of the GNU General Public License as published by 71f374598SGreg Roach * the Free Software Foundation, either version 3 of the License, or 81f374598SGreg Roach * (at your option) any later version. 91f374598SGreg Roach * This program is distributed in the hope that it will be useful, 101f374598SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 111f374598SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 121f374598SGreg Roach * GNU General Public License for more details. 131f374598SGreg Roach * You should have received a copy of the GNU General Public License 141f374598SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 151f374598SGreg Roach */ 161f374598SGreg Roachdeclare(strict_types=1); 171f374598SGreg Roach 181f374598SGreg Roachnamespace Fisharebest\Webtrees\Module; 191f374598SGreg Roach 201f374598SGreg Roachuse Exception; 211f374598SGreg Roachuse Fisharebest\Webtrees\Fact; 228af6bbf8SGreg Roachuse Fisharebest\Webtrees\Family; 231f374598SGreg Roachuse Fisharebest\Webtrees\Functions\Functions; 248af6bbf8SGreg Roachuse Fisharebest\Webtrees\GedcomTag; 251f374598SGreg Roachuse Fisharebest\Webtrees\I18N; 261f374598SGreg Roachuse Fisharebest\Webtrees\Individual; 278af6bbf8SGreg Roachuse Fisharebest\Webtrees\Location; 288d0ebef0SGreg Roachuse Fisharebest\Webtrees\Webtrees; 2939ca88baSGreg Roachuse Illuminate\Support\Collection; 3005ff554cSGreg Roachuse stdClass; 311f374598SGreg Roachuse Symfony\Component\HttpFoundation\JsonResponse; 321f374598SGreg Roachuse Symfony\Component\HttpFoundation\Request; 331f374598SGreg Roach 341f374598SGreg Roach/** 351f374598SGreg Roach * Class PlacesMapModule 361f374598SGreg Roach */ 3737eb8894SGreg Roachclass PlacesModule extends AbstractModule implements ModuleTabInterface 381f374598SGreg Roach{ 3949a243cbSGreg Roach use ModuleTabTrait; 4049a243cbSGreg Roach 411f374598SGreg Roach private static $map_providers = null; 421f374598SGreg Roach private static $map_selections = null; 431f374598SGreg Roach 448af6bbf8SGreg Roach public const ICONS = [ 458af6bbf8SGreg Roach 'BIRT' => ['color' => 'Crimson', 'name' => 'birthday-cake'], 468af6bbf8SGreg Roach 'MARR' => ['color' => 'Green', 'name' => 'venus-mars'], 478af6bbf8SGreg Roach 'DEAT' => ['color' => 'Black', 'name' => 'plus'], 488af6bbf8SGreg Roach 'CENS' => ['color' => 'MediumBlue', 'name' => 'users'], 498af6bbf8SGreg Roach 'RESI' => ['color' => 'MediumBlue', 'name' => 'home'], 508af6bbf8SGreg Roach 'OCCU' => ['color' => 'MediumBlue', 'name' => 'briefcase'], 518af6bbf8SGreg Roach 'GRAD' => ['color' => 'MediumBlue', 'name' => 'graduation-cap'], 528af6bbf8SGreg Roach 'EDUC' => ['color' => 'MediumBlue', 'name' => 'university'], 538af6bbf8SGreg Roach ]; 548af6bbf8SGreg Roach 558af6bbf8SGreg Roach public const DEFAULT_ICON = ['color' => 'Gold', 'name' => 'bullseye ']; 568af6bbf8SGreg Roach 57961ec755SGreg Roach /** 58961ec755SGreg Roach * How should this module be labelled on tabs, menus, etc.? 59961ec755SGreg Roach * 60961ec755SGreg Roach * @return string 61961ec755SGreg Roach */ 6249a243cbSGreg Roach public function title(): string 631f374598SGreg Roach { 64bbb76c12SGreg Roach /* I18N: Name of a module */ 65bbb76c12SGreg Roach return I18N::translate('Places'); 661f374598SGreg Roach } 671f374598SGreg Roach 68961ec755SGreg Roach /** 69961ec755SGreg Roach * A sentence describing what this module does. 70961ec755SGreg Roach * 71961ec755SGreg Roach * @return string 72961ec755SGreg Roach */ 7349a243cbSGreg Roach public function description(): string 741f374598SGreg Roach { 75bbb76c12SGreg Roach /* I18N: Description of the “OSM” module */ 76bbb76c12SGreg Roach return I18N::translate('Show the location of events on a map.'); 771f374598SGreg Roach } 781f374598SGreg Roach 7949a243cbSGreg Roach /** 8049a243cbSGreg Roach * The default position for this tab. It can be changed in the control panel. 8149a243cbSGreg Roach * 8249a243cbSGreg Roach * @return int 8349a243cbSGreg Roach */ 84cbf4b7faSGreg Roach public function defaultTabOrder(): int 85cbf4b7faSGreg Roach { 86353b36abSGreg Roach return 1; 871f374598SGreg Roach } 881f374598SGreg Roach 891f374598SGreg Roach /** {@inheritdoc} */ 908f53f488SRico Sonntag public function hasTabContent(Individual $individual): bool 911f374598SGreg Roach { 921f374598SGreg Roach return true; 931f374598SGreg Roach } 941f374598SGreg Roach 951f374598SGreg Roach /** {@inheritdoc} */ 968f53f488SRico Sonntag public function isGrayedOut(Individual $individual): bool 971f374598SGreg Roach { 981f374598SGreg Roach return false; 991f374598SGreg Roach } 1001f374598SGreg Roach 1011f374598SGreg Roach /** {@inheritdoc} */ 1028f53f488SRico Sonntag public function canLoadAjax(): bool 1031f374598SGreg Roach { 1041f374598SGreg Roach return true; 1051f374598SGreg Roach } 1061f374598SGreg Roach 1071f374598SGreg Roach /** {@inheritdoc} */ 1089b34404bSGreg Roach public function getTabContent(Individual $individual): string 1091f374598SGreg Roach { 110aacdcb0dSGreg Roach return view('modules/places/tab', [ 11105ff554cSGreg Roach 'data' => $this->getMapData($individual), 112aacdcb0dSGreg Roach ]); 1131f374598SGreg Roach } 1141f374598SGreg Roach 1151f374598SGreg Roach /** 116e93111adSRico Sonntag * @param Individual $indi 1171f374598SGreg Roach * 11805ff554cSGreg Roach * @return stdClass 1191f374598SGreg Roach */ 12005ff554cSGreg Roach private function getMapData(Individual $indi): stdClass 1211f374598SGreg Roach { 12205ff554cSGreg Roach $facts = $this->getPersonalFacts($indi); 1231f374598SGreg Roach 1241f374598SGreg Roach $geojson = [ 1251f374598SGreg Roach 'type' => 'FeatureCollection', 1261f374598SGreg Roach 'features' => [], 1271f374598SGreg Roach ]; 12805ff554cSGreg Roach 1291f374598SGreg Roach foreach ($facts as $id => $fact) { 1308af6bbf8SGreg Roach $location = new Location($fact->place()->gedcomName()); 1318af6bbf8SGreg Roach 1328af6bbf8SGreg Roach // Use the co-ordinates from the fact (if they exist). 1338af6bbf8SGreg Roach $latitude = $fact->latitude(); 1348af6bbf8SGreg Roach $longitude = $fact->longitude(); 1358af6bbf8SGreg Roach 1368af6bbf8SGreg Roach // Use the co-ordinates from the location otherwise. 1378af6bbf8SGreg Roach if ($latitude === 0.0 && $longitude === 0.0) { 1388af6bbf8SGreg Roach $latitude = $location->latitude(); 1398af6bbf8SGreg Roach $longitude = $location->longitude(); 1408af6bbf8SGreg Roach } 1418af6bbf8SGreg Roach 1428af6bbf8SGreg Roach $icon = self::ICONS[$fact->getTag()] ?? self::DEFAULT_ICON; 1438af6bbf8SGreg Roach 1448af6bbf8SGreg Roach if ($latitude !== 0.0 || $longitude !== 0.0) { 1451f374598SGreg Roach $geojson['features'][] = [ 1461f374598SGreg Roach 'type' => 'Feature', 1471f374598SGreg Roach 'id' => $id, 1481f374598SGreg Roach 'valid' => true, 1491f374598SGreg Roach 'geometry' => [ 1501f374598SGreg Roach 'type' => 'Point', 151*f465e1eaSGreg Roach 'coordinates' => [$longitude, $latitude], 1521f374598SGreg Roach ], 1531f374598SGreg Roach 'properties' => [ 15405ff554cSGreg Roach 'polyline' => null, 1551f374598SGreg Roach 'icon' => $icon, 1568af6bbf8SGreg Roach 'tooltip' => strip_tags($fact->place()->fullName()), 1578af6bbf8SGreg Roach 'summary' => view('modules/places/event-sidebar', $this->summaryData($indi, $fact)), 1588af6bbf8SGreg Roach 'zoom' => $location->zoom(), 1591f374598SGreg Roach ], 1601f374598SGreg Roach ]; 1611f374598SGreg Roach } 1621f374598SGreg Roach } 1631f374598SGreg Roach 16405ff554cSGreg Roach return (object) $geojson; 1651f374598SGreg Roach } 1661f374598SGreg Roach 1671f374598SGreg Roach /** 16805ff554cSGreg Roach * @param Individual $individual 1698af6bbf8SGreg Roach * @param Fact $fact 1708af6bbf8SGreg Roach * 1718af6bbf8SGreg Roach * @return mixed[] 1728af6bbf8SGreg Roach */ 1738af6bbf8SGreg Roach private function summaryData(Individual $individual, Fact $fact): array 1748af6bbf8SGreg Roach { 1758af6bbf8SGreg Roach $record = $fact->record(); 1768af6bbf8SGreg Roach $name = ''; 1778af6bbf8SGreg Roach $url = ''; 1788af6bbf8SGreg Roach $tag = $fact->label(); 1798af6bbf8SGreg Roach 1808af6bbf8SGreg Roach if ($record instanceof Family) { 1818af6bbf8SGreg Roach // Marriage 18239ca88baSGreg Roach $spouse = $record->spouse($individual); 1838af6bbf8SGreg Roach if ($spouse instanceof Individual) { 1848af6bbf8SGreg Roach $url = $spouse->url(); 18539ca88baSGreg Roach $name = $spouse->fullName(); 1868af6bbf8SGreg Roach } 1878af6bbf8SGreg Roach } elseif ($record !== $individual) { 1888af6bbf8SGreg Roach // Birth of a child 1898af6bbf8SGreg Roach $url = $record->url(); 19039ca88baSGreg Roach $name = $record->fullName(); 1918af6bbf8SGreg Roach $tag = GedcomTag::getLabel('_BIRT_CHIL', $record); 1928af6bbf8SGreg Roach } 1938af6bbf8SGreg Roach 1948af6bbf8SGreg Roach return [ 1958af6bbf8SGreg Roach 'tag' => $tag, 1968af6bbf8SGreg Roach 'url' => $url, 1978af6bbf8SGreg Roach 'name' => $name, 1988af6bbf8SGreg Roach 'value' => $fact->value(), 1998af6bbf8SGreg Roach 'date' => $fact->date()->display(true), 2008af6bbf8SGreg Roach 'place' => $fact->place(), 2018af6bbf8SGreg Roach 'addtag' => false, 2028af6bbf8SGreg Roach ]; 2038af6bbf8SGreg Roach } 2048af6bbf8SGreg Roach 2058af6bbf8SGreg Roach /** 2068af6bbf8SGreg Roach * @param Individual $individual 2071f374598SGreg Roach * 20839ca88baSGreg Roach * @return Collection|Fact[] 2091f374598SGreg Roach * @throws Exception 2101f374598SGreg Roach */ 21139ca88baSGreg Roach private function getPersonalFacts(Individual $individual): Collection 2121f374598SGreg Roach { 21330158ae7SGreg Roach $facts = $individual->facts(); 21439ca88baSGreg Roach 21539ca88baSGreg Roach foreach ($individual->spouseFamilies() as $family) { 21639ca88baSGreg Roach $facts = $facts->merge($family->facts()); 2171f374598SGreg Roach // Add birth of children from this family to the facts array 21839ca88baSGreg Roach foreach ($family->children() as $child) { 219820b62dfSGreg Roach $childsBirth = $child->facts(['BIRT'])->first(); 220392561bbSGreg Roach if ($childsBirth && $childsBirth->place()->gedcomName() !== '') { 22139ca88baSGreg Roach $facts->push($childsBirth); 2221f374598SGreg Roach } 2231f374598SGreg Roach } 2241f374598SGreg Roach } 2251f374598SGreg Roach 2261f374598SGreg Roach Functions::sortFacts($facts); 2271f374598SGreg Roach 22839ca88baSGreg Roach return $facts->filter(function (Fact $item): bool { 2298af6bbf8SGreg Roach return $item->place()->gedcomName() !== ''; 23039ca88baSGreg Roach }); 2311f374598SGreg Roach } 2321f374598SGreg Roach 2331f374598SGreg Roach /** 2341f374598SGreg Roach * @param Request $request 2351f374598SGreg Roach * 2361f374598SGreg Roach * @return JsonResponse 2371f374598SGreg Roach */ 2381f374598SGreg Roach public function getProviderStylesAction(Request $request): JsonResponse 2391f374598SGreg Roach { 2401f374598SGreg Roach $styles = $this->getMapProviderData($request); 2411f374598SGreg Roach 2421f374598SGreg Roach return new JsonResponse($styles); 2431f374598SGreg Roach } 2441f374598SGreg Roach 2451f374598SGreg Roach /** 2461f374598SGreg Roach * @param Request $request 2471f374598SGreg Roach * 2481f374598SGreg Roach * @return array|null 2491f374598SGreg Roach */ 2501f374598SGreg Roach private function getMapProviderData(Request $request) 2511f374598SGreg Roach { 2521f374598SGreg Roach if (self::$map_providers === null) { 2538d0ebef0SGreg Roach $providersFile = WT_ROOT . Webtrees::MODULES_PATH . 'openstreetmap/providers/providers.xml'; 2541f374598SGreg Roach self::$map_selections = [ 2551f374598SGreg Roach 'provider' => $this->getPreference('provider', 'openstreetmap'), 2561f374598SGreg Roach 'style' => $this->getPreference('provider_style', 'mapnik'), 2571f374598SGreg Roach ]; 2581f374598SGreg Roach 2591f374598SGreg Roach try { 2601f374598SGreg Roach $xml = simplexml_load_file($providersFile); 2611f374598SGreg Roach // need to convert xml structure into arrays & strings 2621f374598SGreg Roach foreach ($xml as $provider) { 2631f374598SGreg Roach $style_keys = array_map( 26418d7a90dSGreg Roach function (string $item): string { 2651f374598SGreg Roach return preg_replace('/[^a-z\d]/i', '', strtolower($item)); 2661f374598SGreg Roach }, 2671f374598SGreg Roach (array) $provider->styles 2681f374598SGreg Roach ); 2691f374598SGreg Roach 2701f374598SGreg Roach $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name)); 2711f374598SGreg Roach 2721f374598SGreg Roach self::$map_providers[$key] = [ 2731f374598SGreg Roach 'name' => (string) $provider->name, 2741f374598SGreg Roach 'styles' => array_combine($style_keys, (array) $provider->styles), 2751f374598SGreg Roach ]; 2761f374598SGreg Roach } 2771f374598SGreg Roach } catch (Exception $ex) { 2781f374598SGreg Roach // Default provider is OpenStreetMap 2791f374598SGreg Roach self::$map_selections = [ 2801f374598SGreg Roach 'provider' => 'openstreetmap', 2811f374598SGreg Roach 'style' => 'mapnik', 2821f374598SGreg Roach ]; 2831f374598SGreg Roach self::$map_providers = [ 2841f374598SGreg Roach 'openstreetmap' => [ 2851f374598SGreg Roach 'name' => 'OpenStreetMap', 2861f374598SGreg Roach 'styles' => ['mapnik' => 'Mapnik'], 2871f374598SGreg Roach ], 2881f374598SGreg Roach ]; 2891f374598SGreg Roach }; 2901f374598SGreg Roach } 2911f374598SGreg Roach 2921f374598SGreg Roach //Ugly!!! 2931f374598SGreg Roach switch ($request->get('action')) { 2941f374598SGreg Roach case 'BaseData': 2951f374598SGreg Roach $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']]; 2961f374598SGreg Roach $payload = [ 2971f374598SGreg Roach 'selectedProvIndex' => self::$map_selections['provider'], 2981f374598SGreg Roach 'selectedProvName' => self::$map_providers[self::$map_selections['provider']]['name'], 2991f374598SGreg Roach 'selectedStyleName' => $varName, 3001f374598SGreg Roach ]; 3011f374598SGreg Roach break; 3021f374598SGreg Roach case 'ProviderStyles': 3031f374598SGreg Roach $provider = $request->get('provider', 'openstreetmap'); 3041f374598SGreg Roach $payload = self::$map_providers[$provider]['styles']; 3051f374598SGreg Roach break; 3061f374598SGreg Roach case 'AdminConfig': 3071f374598SGreg Roach $providers = []; 3081f374598SGreg Roach foreach (self::$map_providers as $key => $provider) { 3091f374598SGreg Roach $providers[$key] = $provider['name']; 3101f374598SGreg Roach } 3111f374598SGreg Roach $payload = [ 3121f374598SGreg Roach 'providers' => $providers, 3131f374598SGreg Roach 'selectedProv' => self::$map_selections['provider'], 3141f374598SGreg Roach 'styles' => self::$map_providers[self::$map_selections['provider']]['styles'], 3151f374598SGreg Roach 'selectedStyle' => self::$map_selections['style'], 3161f374598SGreg Roach ]; 3171f374598SGreg Roach break; 3181f374598SGreg Roach default: 3191f374598SGreg Roach $payload = null; 3201f374598SGreg Roach } 3211f374598SGreg Roach 3221f374598SGreg Roach return $payload; 3231f374598SGreg Roach } 3241f374598SGreg Roach} 325