xref: /webtrees/app/Module/PedigreeMapModule.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\Exceptions\IndividualAccessDeniedException;
22use Fisharebest\Webtrees\Exceptions\IndividualNotFoundException;
23use Fisharebest\Webtrees\Fact;
24use Fisharebest\Webtrees\FactLocation;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Individual;
27use Fisharebest\Webtrees\Menu;
28use Fisharebest\Webtrees\Services\ChartService;
29use Fisharebest\Webtrees\Tree;
30use Fisharebest\Webtrees\Webtrees;
31use Symfony\Component\HttpFoundation\JsonResponse;
32use Symfony\Component\HttpFoundation\Request;
33use Symfony\Component\HttpFoundation\Response;
34
35/**
36 * Class PedigreeMapModule
37 */
38class PedigreeMapModule extends AbstractModule implements ModuleInterface, ModuleChartInterface
39{
40    use ModuleChartTrait;
41
42    private const LINE_COLORS = [
43        '#FF0000',
44        // Red
45        '#00FF00',
46        // Green
47        '#0000FF',
48        // Blue
49        '#FFB300',
50        // Gold
51        '#00FFFF',
52        // Cyan
53        '#FF00FF',
54        // Purple
55        '#7777FF',
56        // Light blue
57        '#80FF80'
58        // Light green
59    ];
60
61    private static $map_providers  = null;
62    private static $map_selections = null;
63
64    /** {@inheritdoc} */
65    public function title(): string
66    {
67        /* I18N: Name of a module */
68        return I18N::translate('Pedigree map');
69    }
70
71    /** {@inheritdoc} */
72    public function description(): string
73    {
74        /* I18N: Description of the “OSM” module */
75        return I18N::translate('Show the birthplace of ancestors on a map.');
76    }
77
78    /**
79     * Return a menu item for this chart.
80     *
81     * @param Individual $individual
82     *
83     * @return Menu|null
84     */
85    public function getChartMenu(Individual $individual): ?Menu
86    {
87        return new Menu(
88            I18N::translate('Pedigree map'),
89            route('module', [
90                'module' => $this->getName(),
91                'action' => 'PedigreeMap',
92                'xref'   => $individual->xref(),
93                'ged'    => $individual->tree()->name(),
94            ]),
95            'menu-chart-pedigreemap',
96            ['rel' => 'nofollow']
97        );
98    }
99
100    /**
101     * Return a menu item for this chart - for use in individual boxes.
102     *
103     * @param Individual $individual
104     *
105     * @return Menu|null
106     */
107    public function getBoxChartMenu(Individual $individual): ?Menu
108    {
109        return $this->getChartMenu($individual);
110    }
111
112    /**
113     * @param Request      $request
114     * @param Tree         $tree
115     * @param ChartService $chart_service
116     *
117     * @return JsonResponse
118     */
119    public function getMapDataAction(Request $request, Tree $tree, ChartService $chart_service): JsonResponse
120    {
121        $xref        = $request->get('reference');
122        $indi        = Individual::getInstance($xref, $tree);
123        $color_count = count(self::LINE_COLORS);
124
125        $facts = $this->getPedigreeMapFacts($request, $tree, $chart_service);
126
127        $geojson = [
128            'type'     => 'FeatureCollection',
129            'features' => [],
130        ];
131
132        $sosa_points = [];
133
134        foreach ($facts as $id => $fact) {
135            $event = new FactLocation($fact, $indi);
136            $icon  = $event->getIconDetails();
137            if ($event->knownLatLon()) {
138                $polyline         = null;
139                $color            = self::LINE_COLORS[log($id, 2) % $color_count];
140                $icon['color']    = $color; //make icon color the same as the line
141                $sosa_points[$id] = $event->getLatLonJSArray();
142                $sosa_parent      = intdiv($id, 2);
143                if (array_key_exists($sosa_parent, $sosa_points)) {
144                    // Would like to use a GeometryCollection to hold LineStrings
145                    // rather than generate polylines but the MarkerCluster library
146                    // doesn't seem to like them
147                    $polyline = [
148                        'points'  => [
149                            $sosa_points[$sosa_parent],
150                            $event->getLatLonJSArray(),
151                        ],
152                        'options' => [
153                            'color' => $color,
154                        ],
155                    ];
156                }
157                $geojson['features'][] = [
158                    'type'       => 'Feature',
159                    'id'         => $id,
160                    'valid'      => true,
161                    'geometry'   => [
162                        'type'        => 'Point',
163                        'coordinates' => $event->getGeoJsonCoords(),
164                    ],
165                    'properties' => [
166                        'polyline' => $polyline,
167                        'icon'     => $icon,
168                        'tooltip'  => $event->toolTip(),
169                        'summary'  => view('modules/pedigree-map/event-sidebar', $event->shortSummary('pedigree', $id)),
170                        'zoom'     => (int) $event->getZoom(),
171                    ],
172                ];
173            }
174        }
175
176        $code = empty($facts) ? Response::HTTP_NO_CONTENT : Response::HTTP_OK;
177
178        return new JsonResponse($geojson, $code);
179    }
180
181    /**
182     * @param Request      $request
183     * @param Tree         $tree
184     * @param ChartService $chart_service
185     *
186     * @return array
187     */
188    private function getPedigreeMapFacts(Request $request, Tree $tree, ChartService $chart_service): array
189    {
190        $xref        = $request->get('reference');
191        $individual  = Individual::getInstance($xref, $tree);
192        $generations = (int) $request->get(
193            'generations',
194            $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS')
195        );
196        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
197        $facts       = [];
198        foreach ($ancestors as $sosa => $person) {
199            if ($person->canShow()) {
200                $birth = $person->getFirstFact('BIRT');
201                if ($birth instanceof Fact && !$birth->place()->isEmpty()) {
202                    $facts[$sosa] = $birth;
203                }
204            }
205        }
206
207        return $facts;
208    }
209
210    /**
211     * @param Request $request
212     *
213     * @return JsonResponse
214     */
215    public function getProviderStylesAction(Request $request): JsonResponse
216    {
217        $styles = $this->getMapProviderData($request);
218
219        return new JsonResponse($styles);
220    }
221
222    /**
223     * @param Request $request
224     *
225     * @return array|null
226     */
227    private function getMapProviderData(Request $request)
228    {
229        if (self::$map_providers === null) {
230            $providersFile        = WT_ROOT . Webtrees::MODULES_PATH . 'openstreetmap/providers/providers.xml';
231            self::$map_selections = [
232                'provider' => $this->getPreference('provider', 'openstreetmap'),
233                'style'    => $this->getPreference('provider_style', 'mapnik'),
234            ];
235
236            try {
237                $xml = simplexml_load_file($providersFile);
238                // need to convert xml structure into arrays & strings
239                foreach ($xml as $provider) {
240                    $style_keys = array_map(
241                        function (string $item): string {
242                            return preg_replace('/[^a-z\d]/i', '', strtolower($item));
243                        },
244                        (array) $provider->styles
245                    );
246
247                    $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name));
248
249                    self::$map_providers[$key] = [
250                        'name'   => (string) $provider->name,
251                        'styles' => array_combine($style_keys, (array) $provider->styles),
252                    ];
253                }
254            } catch (Exception $ex) {
255                // Default provider is OpenStreetMap
256                self::$map_selections = [
257                    'provider' => 'openstreetmap',
258                    'style'    => 'mapnik',
259                ];
260                self::$map_providers = [
261                    'openstreetmap' => [
262                        'name'   => 'OpenStreetMap',
263                        'styles' => ['mapnik' => 'Mapnik'],
264                    ],
265                ];
266            };
267        }
268
269        //Ugly!!!
270        switch ($request->get('action')) {
271            case 'BaseData':
272                $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']];
273                $payload = [
274                    'selectedProvIndex' => self::$map_selections['provider'],
275                    'selectedProvName'  => self::$map_providers[self::$map_selections['provider']]['name'],
276                    'selectedStyleName' => $varName,
277                ];
278                break;
279            case 'ProviderStyles':
280                $provider = $request->get('provider', 'openstreetmap');
281                $payload  = self::$map_providers[$provider]['styles'];
282                break;
283            case 'AdminConfig':
284                $providers = [];
285                foreach (self::$map_providers as $key => $provider) {
286                    $providers[$key] = $provider['name'];
287                }
288                $payload = [
289                    'providers'     => $providers,
290                    'selectedProv'  => self::$map_selections['provider'],
291                    'styles'        => self::$map_providers[self::$map_selections['provider']]['styles'],
292                    'selectedStyle' => self::$map_selections['style'],
293                ];
294                break;
295            default:
296                $payload = null;
297        }
298
299        return $payload;
300    }
301
302    /**
303     * @param Request $request
304     * @param Tree    $tree
305     *
306     * @return object
307     */
308    public function getPedigreeMapAction(Request $request, Tree $tree)
309    {
310        $xref           = $request->get('xref', '');
311        $individual     = Individual::getInstance($xref, $tree);
312        $maxgenerations = $tree->getPreference('MAX_PEDIGREE_GENERATIONS');
313        $generations    = $request->get('generations', $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS'));
314
315        if ($individual === null) {
316            throw new IndividualNotFoundException();
317        }
318
319        if (!$individual->canShow()) {
320            throw new IndividualAccessDeniedException();
321        }
322
323        return $this->viewResponse('modules/pedigree-map/pedigree-map-page', [
324            'module'         => $this->getName(),
325            /* I18N: %s is an individual’s name */
326            'title'          => I18N::translate('Pedigree map of %s', $individual->getFullName()),
327            'tree'           => $tree,
328            'individual'     => $individual,
329            'generations'    => $generations,
330            'maxgenerations' => $maxgenerations,
331            'map'            => view(
332                'modules/pedigree-map/pedigree-map',
333                [
334                    'module'      => $this->getName(),
335                    'ref'         => $individual->xref(),
336                    'type'        => 'pedigree',
337                    'generations' => $generations,
338                ]
339            )
340        ]);
341    }
342}
343