xref: /webtrees/app/Module/PlacesModule.php (revision 895230eed7521b5cd885b90d4f5310405ff0b69a)
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 ModuleInterface, ModuleTabInterface
35{
36    use ModuleTabTrait;
37
38    private static $map_providers  = null;
39    private static $map_selections = null;
40
41    /** {@inheritdoc} */
42    public function title(): string
43    {
44        /* I18N: Name of a module */
45        return I18N::translate('Places');
46    }
47
48    /** {@inheritdoc} */
49    public function description(): string
50    {
51        /* I18N: Description of the “OSM” module */
52        return I18N::translate('Show the location of events on a map.');
53    }
54
55    /**
56     * The default position for this tab.  It can be changed in the control panel.
57     *
58     * @return int
59     */
60    public function defaultTabOrder(): int
61    {
62        return 10;
63    }
64
65    /** {@inheritdoc} */
66    public function hasTabContent(Individual $individual): bool
67    {
68        return true;
69    }
70
71    /** {@inheritdoc} */
72    public function isGrayedOut(Individual $individual): bool
73    {
74        return false;
75    }
76
77    /** {@inheritdoc} */
78    public function canLoadAjax(): bool
79    {
80        return true;
81    }
82
83    /** {@inheritdoc} */
84    public function getTabContent(Individual $individual): string
85    {
86        return view('modules/places/tab', [
87            'data' => $this->getMapData($individual),
88        ]);
89    }
90
91    /**
92     * @param Individual $indi
93     *
94     * @return stdClass
95     */
96    private function getMapData(Individual $indi): stdClass
97    {
98        $facts = $this->getPersonalFacts($indi);
99
100        $geojson = [
101            'type'     => 'FeatureCollection',
102            'features' => [],
103        ];
104
105        foreach ($facts as $id => $fact) {
106            $event = new FactLocation($fact, $indi);
107            $icon  = $event->getIconDetails();
108            if ($event->knownLatLon()) {
109                $geojson['features'][] = [
110                    'type'       => 'Feature',
111                    'id'         => $id,
112                    'valid'      => true,
113                    'geometry'   => [
114                        'type'        => 'Point',
115                        'coordinates' => $event->getGeoJsonCoords(),
116                    ],
117                    'properties' => [
118                        'polyline' => null,
119                        'icon'     => $icon,
120                        'tooltip'  => $event->toolTip(),
121                        'summary'  => view(
122                            'modules/places/event-sidebar',
123                            $event->shortSummary('individual', $id)
124                        ),
125                        'zoom'     => (int) $event->getZoom(),
126                    ],
127                ];
128            }
129        }
130
131        return (object) $geojson;
132    }
133
134    /**
135     * @param Individual $individual
136     *
137     * @return array
138     * @throws Exception
139     */
140    private function getPersonalFacts(Individual $individual): array
141    {
142        $facts = $individual->facts();
143        foreach ($individual->getSpouseFamilies() as $family) {
144            $facts = array_merge($facts, $family->facts());
145            // Add birth of children from this family to the facts array
146            foreach ($family->getChildren() as $child) {
147                $childsBirth = $child->getFirstFact('BIRT');
148                if ($childsBirth && !$childsBirth->place()->isEmpty()) {
149                    $facts[] = $childsBirth;
150                }
151            }
152        }
153
154        Functions::sortFacts($facts);
155
156        $useable_facts = array_filter(
157            $facts,
158            function (Fact $item): bool {
159                return !$item->place()->isEmpty();
160            }
161        );
162
163        return array_values($useable_facts);
164    }
165
166    /**
167     * @param Request $request
168     *
169     * @return JsonResponse
170     */
171    public function getProviderStylesAction(Request $request): JsonResponse
172    {
173        $styles = $this->getMapProviderData($request);
174
175        return new JsonResponse($styles);
176    }
177
178    /**
179     * @param Request $request
180     *
181     * @return array|null
182     */
183    private function getMapProviderData(Request $request)
184    {
185        if (self::$map_providers === null) {
186            $providersFile        = WT_ROOT . Webtrees::MODULES_PATH . 'openstreetmap/providers/providers.xml';
187            self::$map_selections = [
188                'provider' => $this->getPreference('provider', 'openstreetmap'),
189                'style'    => $this->getPreference('provider_style', 'mapnik'),
190            ];
191
192            try {
193                $xml = simplexml_load_file($providersFile);
194                // need to convert xml structure into arrays & strings
195                foreach ($xml as $provider) {
196                    $style_keys = array_map(
197                        function (string $item): string {
198                            return preg_replace('/[^a-z\d]/i', '', strtolower($item));
199                        },
200                        (array) $provider->styles
201                    );
202
203                    $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name));
204
205                    self::$map_providers[$key] = [
206                        'name'   => (string) $provider->name,
207                        'styles' => array_combine($style_keys, (array) $provider->styles),
208                    ];
209                }
210            } catch (Exception $ex) {
211                // Default provider is OpenStreetMap
212                self::$map_selections = [
213                    'provider' => 'openstreetmap',
214                    'style'    => 'mapnik',
215                ];
216                self::$map_providers = [
217                    'openstreetmap' => [
218                        'name'   => 'OpenStreetMap',
219                        'styles' => ['mapnik' => 'Mapnik'],
220                    ],
221                ];
222            };
223        }
224
225        //Ugly!!!
226        switch ($request->get('action')) {
227            case 'BaseData':
228                $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']];
229                $payload = [
230                    'selectedProvIndex' => self::$map_selections['provider'],
231                    'selectedProvName'  => self::$map_providers[self::$map_selections['provider']]['name'],
232                    'selectedStyleName' => $varName,
233                ];
234                break;
235            case 'ProviderStyles':
236                $provider = $request->get('provider', 'openstreetmap');
237                $payload  = self::$map_providers[$provider]['styles'];
238                break;
239            case 'AdminConfig':
240                $providers = [];
241                foreach (self::$map_providers as $key => $provider) {
242                    $providers[$key] = $provider['name'];
243                }
244                $payload = [
245                    'providers'     => $providers,
246                    'selectedProv'  => self::$map_selections['provider'],
247                    'styles'        => self::$map_providers[self::$map_selections['provider']]['styles'],
248                    'selectedStyle' => self::$map_selections['style'],
249                ];
250                break;
251            default:
252                $payload = null;
253        }
254
255        return $payload;
256    }
257}
258