xref: /webtrees/app/Module/PlacesModule.php (revision 0a661b58997b16d024fcfba9569c11325cfd48fe)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 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\Auth;
22use Fisharebest\Webtrees\Database;
23use Fisharebest\Webtrees\Fact;
24use Fisharebest\Webtrees\FactLocation;
25use Fisharebest\Webtrees\Functions\Functions;
26use Fisharebest\Webtrees\I18N;
27use Fisharebest\Webtrees\Individual;
28use Symfony\Component\HttpFoundation\JsonResponse;
29use Symfony\Component\HttpFoundation\Request;
30
31/**
32 * Class PlacesMapModule
33 */
34class PlacesModule extends AbstractModule implements ModuleTabInterface
35{
36    const OSM_MIN_ZOOM = 2;
37    const LINE_COLORS  = [
38        '#FF0000',
39        // Red
40        '#00FF00',
41        // Green
42        '#0000FF',
43        // Blue
44        '#FFB300',
45        // Gold
46        '#00FFFF',
47        // Cyan
48        '#FF00FF',
49        // Purple
50        '#7777FF',
51        // Light blue
52        '#80FF80'
53        // Light green
54    ];
55
56    private static $map_providers  = null;
57    private static $map_selections = null;
58
59    /** {@inheritdoc} */
60    public function getTitle()
61    {
62        return /* I18N: Name of a module */
63            I18N::translate('Places');
64    }
65
66    /** {@inheritdoc} */
67    public function getDescription()
68    {
69        return /* I18N: Description of the “OSM” module */
70            I18N::translate('Show the location of events on a map.');
71    }
72
73    /** {@inheritdoc} */
74    public function defaultAccessLevel()
75    {
76        return Auth::PRIV_PRIVATE;
77    }
78
79    /** {@inheritdoc} */
80    public function defaultTabOrder()
81    {
82        return 4;
83    }
84
85    /** {@inheritdoc} */
86    public function hasTabContent(Individual $individual)
87    {
88        return true;
89    }
90
91    /** {@inheritdoc} */
92    public function isGrayedOut(Individual $individual)
93    {
94        return false;
95    }
96
97    /** {@inheritdoc} */
98    public function canLoadAjax()
99    {
100        return true;
101    }
102
103    /** {@inheritdoc} */
104    public function getTabContent(Individual $individual)
105    {
106        return view('modules/places/tab', [
107            'module' => $this->getName(),
108            'ref'    => $individual->getXref(),
109            'type'   => 'individual',
110        ]);
111    }
112
113    /**
114     * @param Request $request
115     *
116     * @return JsonResponse
117     */
118    public function getBaseDataAction(Request $request): JsonResponse
119    {
120        $provider = $this->getMapProviderData($request);
121        $style    = $provider['selectedStyleName'] = '' ? '' : '.' . $provider['selectedStyleName'];
122
123        switch ($provider['selectedProvIndex']) {
124            case 'mapbox':
125                $providerOptions = [
126                    'id'          => $this->getPreference('mapbox_id'),
127                    'accessToken' => $this->getPreference('mapbox_token'),
128                ];
129                break;
130            case 'here':
131                $providerOptions = [
132                    'app_id'   => $this->getPreference('here_appid'),
133                    'app_code' => $this->getPreference('here_appcode'),
134                ];
135                break;
136            default:
137                $providerOptions = [];
138        };
139
140        $options = [
141            'minZoom'         => self::OSM_MIN_ZOOM,
142            'providerName'    => $provider['selectedProvName'] . $style,
143            'providerOptions' => $providerOptions,
144            'animate'         => $this->getPreference('map_animate', 0),
145            'I18N'            => [
146                'zoomInTitle'  => I18N::translate('Zoom in'),
147                'zoomOutTitle' => I18N::translate('Zoom out'),
148                'reset'        => I18N::translate('Reset to initial map state'),
149                'noData'       => I18N::translate('No mappable items'),
150                'error'        => I18N::translate('An unknown error occurred'),
151            ],
152        ];
153
154        return new JsonResponse($options);
155    }
156
157    /**
158     * @param Request $request
159     *
160     * @return JsonResponse
161     * @throws Exception
162     */
163    public function getMapDataAction(Request $request): JsonResponse
164    {
165        $xref  = $request->get('reference');
166        $tree  = $request->attributes->get('tree');
167        $indi  = Individual::getInstance($xref, $tree);
168        $facts = $this->getPersonalFacts($request);
169
170        $geojson = [
171            'type'     => 'FeatureCollection',
172            'features' => [],
173        ];
174        if (empty($facts)) {
175            $code = 204;
176        } else {
177            $code = 200;
178            foreach ($facts as $id => $fact) {
179                $event = new FactLocation($fact, $indi);
180                $icon  = $event->getIconDetails();
181                if ($event->knownLatLon()) {
182                    $polyline              = null;
183                    $geojson['features'][] = [
184                        'type'       => 'Feature',
185                        'id'         => $id,
186                        'valid'      => true,
187                        'geometry'   => [
188                            'type'        => 'Point',
189                            'coordinates' => $event->getGeoJsonCoords(),
190                        ],
191                        'properties' => [
192                            'polyline' => $polyline,
193                            'icon'     => $icon,
194                            'tooltip'  => $event->toolTip(),
195                            'summary'  => view(
196                                'modules/places/event-sidebar',
197                                $event->shortSummary('individual', $id)
198                            ),
199                            'zoom'     => (int) $event->getZoom(),
200                        ],
201                    ];
202                }
203            }
204        }
205
206        return new JsonResponse($geojson, $code);
207    }
208
209    /**
210     * @param Request $request
211     *
212     * @return array
213     * @throws Exception
214     */
215    private function getPersonalFacts(Request $request)
216    {
217        $xref       = $request->get('reference');
218        $tree       = $request->attributes->get('tree');
219        $individual = Individual::getInstance($xref, $tree);
220        $facts      = $individual->getFacts();
221        foreach ($individual->getSpouseFamilies() as $family) {
222            $facts = array_merge($facts, $family->getFacts());
223            // Add birth of children from this family to the facts array
224            foreach ($family->getChildren() as $child) {
225                $childsBirth = $child->getFirstFact('BIRT');
226                if ($childsBirth && !$childsBirth->getPlace()->isEmpty()) {
227                    $facts[] = $childsBirth;
228                }
229            }
230        }
231
232        Functions::sortFacts($facts);
233
234        $useable_facts = array_filter(
235            $facts,
236            function (Fact $item) {
237                return !$item->getPlace()->isEmpty();
238            }
239        );
240
241        return array_values($useable_facts);
242    }
243
244    /**
245     * @param Request $request
246     *
247     * @return JsonResponse
248     */
249    public function getProviderStylesAction(Request $request): JsonResponse
250    {
251        $styles = $this->getMapProviderData($request);
252
253        return new JsonResponse($styles);
254    }
255
256    /**
257     * @param Request $request
258     *
259     * @return array|null
260     */
261    private function getMapProviderData(Request $request)
262    {
263        if (self::$map_providers === null) {
264            $providersFile        = WT_ROOT . WT_MODULES_DIR . 'openstreetmap/providers/providers.xml';
265            self::$map_selections = [
266                'provider' => $this->getPreference('provider', 'openstreetmap'),
267                'style'    => $this->getPreference('provider_style', 'mapnik'),
268            ];
269
270            try {
271                $xml = simplexml_load_file($providersFile);
272                // need to convert xml structure into arrays & strings
273                foreach ($xml as $provider) {
274                    $style_keys = array_map(
275                        function ($item) {
276                            return preg_replace('/[^a-z\d]/i', '', strtolower($item));
277                        },
278                        (array) $provider->styles
279                    );
280
281                    $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name));
282
283                    self::$map_providers[$key] = [
284                        'name'   => (string) $provider->name,
285                        'styles' => array_combine($style_keys, (array) $provider->styles),
286                    ];
287                }
288            } catch (Exception $ex) {
289                // Default provider is OpenStreetMap
290                self::$map_selections = [
291                    'provider' => 'openstreetmap',
292                    'style'    => 'mapnik',
293                ];
294                self::$map_providers  = [
295                    'openstreetmap' => [
296                        'name'   => 'OpenStreetMap',
297                        'styles' => ['mapnik' => 'Mapnik'],
298                    ],
299                ];
300            };
301        }
302
303        //Ugly!!!
304        switch ($request->get('action')) {
305            case 'BaseData':
306                $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']];
307                $payload = [
308                    'selectedProvIndex' => self::$map_selections['provider'],
309                    'selectedProvName'  => self::$map_providers[self::$map_selections['provider']]['name'],
310                    'selectedStyleName' => $varName,
311                ];
312                break;
313            case 'ProviderStyles':
314                $provider = $request->get('provider', 'openstreetmap');
315                $payload  = self::$map_providers[$provider]['styles'];
316                break;
317            case 'AdminConfig':
318                $providers = [];
319                foreach (self::$map_providers as $key => $provider) {
320                    $providers[$key] = $provider['name'];
321                }
322                $payload = [
323                    'providers'     => $providers,
324                    'selectedProv'  => self::$map_selections['provider'],
325                    'styles'        => self::$map_providers[self::$map_selections['provider']]['styles'],
326                    'selectedStyle' => self::$map_selections['style'],
327                ];
328                break;
329            default:
330                $payload = null;
331        }
332
333        return $payload;
334    }
335
336    /**
337     * @param $parent_id
338     * @param $placename
339     * @param $places
340     *
341     * @throws Exception
342     */
343    private function buildLevel($parent_id, $placename, &$places)
344    {
345        $level = array_search('', $placename);
346        $rows  = (array) Database::prepare(
347            "SELECT pl_level, pl_id, pl_place, pl_long, pl_lati, pl_zoom, pl_icon FROM `##placelocation` WHERE pl_parent_id=? ORDER BY pl_place"
348        )
349            ->execute([$parent_id])
350            ->fetchAll(\PDO::FETCH_ASSOC);
351
352        if (!empty($rows)) {
353            foreach ($rows as $row) {
354                $index             = $row['pl_id'];
355                $placename[$level] = $row['pl_place'];
356                $places[]          = array_merge([$row['pl_level']], $placename, array_splice($row, 3));
357                $this->buildLevel($index, $placename, $places);
358            }
359        }
360    }
361}
362