xref: /webtrees/app/Module/PlacesModule.php (revision 4947e7089adb08f24bf1eef67994a106a8df60aa)
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