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