xref: /webtrees/app/Module/PedigreeMapModule.php (revision 961ec7555e8fa1828ea16f2a1fd6a74f140ce707)
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    /**
65     * How should this module be labelled on tabs, menus, etc.?
66     *
67     * @return string
68     */
69    public function title(): string
70    {
71        /* I18N: Name of a module */
72        return I18N::translate('Pedigree map');
73    }
74
75    /**
76     * A sentence describing what this module does.
77     *
78     * @return string
79     */
80    public function description(): string
81    {
82        /* I18N: Description of the “OSM” module */
83        return I18N::translate('Show the birthplace of ancestors on a map.');
84    }
85
86    /**
87     * Return a menu item for this chart - for use in individual boxes.
88     *
89     * @param Individual $individual
90     *
91     * @return Menu|null
92     */
93    public function chartMenuIndividual(Individual $individual): ?Menu
94    {
95        return $this->chartMenu($individual);
96    }
97
98    /**
99     * The title for a specific instance of this chart.
100     *
101     * @param Individual $individual
102     *
103     * @return string
104     */
105    public function chartTitle(Individual $individual): string
106    {
107        /* I18N: %s is an individual’s name */
108        return I18N::translate('Pedigree map of %s', $individual->getFullName());
109    }
110
111    /**
112     * The URL for this chart.
113     *
114     * @param Individual $individual
115     * @param string[]   $parameters
116     *
117     * @return string
118     */
119    public function chartUrl(Individual $individual, array $parameters = []): string
120    {
121        return route('module', [
122            'module' => $this->getName(),
123            'action' => 'PedigreeMap',
124            'xref'   => $individual->xref(),
125            'ged'    => $individual->tree()->name(),
126        ] + $parameters);
127    }
128
129    /**
130     * CSS class for the URL.
131     *
132     * @return string
133     */
134    public function chartUrlClasss(): string
135    {
136        return 'menu-chart-pedigreemap';
137    }
138
139    /**
140     * @param Request      $request
141     * @param Tree         $tree
142     * @param ChartService $chart_service
143     *
144     * @return JsonResponse
145     */
146    public function getMapDataAction(Request $request, Tree $tree, ChartService $chart_service): JsonResponse
147    {
148        $xref        = $request->get('reference');
149        $indi        = Individual::getInstance($xref, $tree);
150        $color_count = count(self::LINE_COLORS);
151
152        $facts = $this->getPedigreeMapFacts($request, $tree, $chart_service);
153
154        $geojson = [
155            'type'     => 'FeatureCollection',
156            'features' => [],
157        ];
158
159        $sosa_points = [];
160
161        foreach ($facts as $id => $fact) {
162            $event = new FactLocation($fact, $indi);
163            $icon  = $event->getIconDetails();
164            if ($event->knownLatLon()) {
165                $polyline         = null;
166                $color            = self::LINE_COLORS[log($id, 2) % $color_count];
167                $icon['color']    = $color; //make icon color the same as the line
168                $sosa_points[$id] = $event->getLatLonJSArray();
169                $sosa_parent      = intdiv($id, 2);
170                if (array_key_exists($sosa_parent, $sosa_points)) {
171                    // Would like to use a GeometryCollection to hold LineStrings
172                    // rather than generate polylines but the MarkerCluster library
173                    // doesn't seem to like them
174                    $polyline = [
175                        'points'  => [
176                            $sosa_points[$sosa_parent],
177                            $event->getLatLonJSArray(),
178                        ],
179                        'options' => [
180                            'color' => $color,
181                        ],
182                    ];
183                }
184                $geojson['features'][] = [
185                    'type'       => 'Feature',
186                    'id'         => $id,
187                    'valid'      => true,
188                    'geometry'   => [
189                        'type'        => 'Point',
190                        'coordinates' => $event->getGeoJsonCoords(),
191                    ],
192                    'properties' => [
193                        'polyline' => $polyline,
194                        'icon'     => $icon,
195                        'tooltip'  => $event->toolTip(),
196                        'summary'  => view('modules/pedigree-map/event-sidebar', $event->shortSummary('pedigree', $id)),
197                        'zoom'     => (int) $event->getZoom(),
198                    ],
199                ];
200            }
201        }
202
203        $code = empty($facts) ? Response::HTTP_NO_CONTENT : Response::HTTP_OK;
204
205        return new JsonResponse($geojson, $code);
206    }
207
208    /**
209     * @param Request      $request
210     * @param Tree         $tree
211     * @param ChartService $chart_service
212     *
213     * @return array
214     */
215    private function getPedigreeMapFacts(Request $request, Tree $tree, ChartService $chart_service): array
216    {
217        $xref        = $request->get('reference');
218        $individual  = Individual::getInstance($xref, $tree);
219        $generations = (int) $request->get(
220            'generations',
221            $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS')
222        );
223        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
224        $facts       = [];
225        foreach ($ancestors as $sosa => $person) {
226            if ($person->canShow()) {
227                $birth = $person->getFirstFact('BIRT');
228                if ($birth instanceof Fact && !$birth->place()->isEmpty()) {
229                    $facts[$sosa] = $birth;
230                }
231            }
232        }
233
234        return $facts;
235    }
236
237    /**
238     * @param Request $request
239     *
240     * @return JsonResponse
241     */
242    public function getProviderStylesAction(Request $request): JsonResponse
243    {
244        $styles = $this->getMapProviderData($request);
245
246        return new JsonResponse($styles);
247    }
248
249    /**
250     * @param Request $request
251     *
252     * @return array|null
253     */
254    private function getMapProviderData(Request $request)
255    {
256        if (self::$map_providers === null) {
257            $providersFile        = WT_ROOT . Webtrees::MODULES_PATH . 'openstreetmap/providers/providers.xml';
258            self::$map_selections = [
259                'provider' => $this->getPreference('provider', 'openstreetmap'),
260                'style'    => $this->getPreference('provider_style', 'mapnik'),
261            ];
262
263            try {
264                $xml = simplexml_load_file($providersFile);
265                // need to convert xml structure into arrays & strings
266                foreach ($xml as $provider) {
267                    $style_keys = array_map(
268                        function (string $item): string {
269                            return preg_replace('/[^a-z\d]/i', '', strtolower($item));
270                        },
271                        (array) $provider->styles
272                    );
273
274                    $key = preg_replace('/[^a-z\d]/i', '', strtolower((string) $provider->name));
275
276                    self::$map_providers[$key] = [
277                        'name'   => (string) $provider->name,
278                        'styles' => array_combine($style_keys, (array) $provider->styles),
279                    ];
280                }
281            } catch (Exception $ex) {
282                // Default provider is OpenStreetMap
283                self::$map_selections = [
284                    'provider' => 'openstreetmap',
285                    'style'    => 'mapnik',
286                ];
287                self::$map_providers  = [
288                    'openstreetmap' => [
289                        'name'   => 'OpenStreetMap',
290                        'styles' => ['mapnik' => 'Mapnik'],
291                    ],
292                ];
293            };
294        }
295
296        //Ugly!!!
297        switch ($request->get('action')) {
298            case 'BaseData':
299                $varName = (self::$map_selections['style'] === '') ? '' : self::$map_providers[self::$map_selections['provider']]['styles'][self::$map_selections['style']];
300                $payload = [
301                    'selectedProvIndex' => self::$map_selections['provider'],
302                    'selectedProvName'  => self::$map_providers[self::$map_selections['provider']]['name'],
303                    'selectedStyleName' => $varName,
304                ];
305                break;
306            case 'ProviderStyles':
307                $provider = $request->get('provider', 'openstreetmap');
308                $payload  = self::$map_providers[$provider]['styles'];
309                break;
310            case 'AdminConfig':
311                $providers = [];
312                foreach (self::$map_providers as $key => $provider) {
313                    $providers[$key] = $provider['name'];
314                }
315                $payload = [
316                    'providers'     => $providers,
317                    'selectedProv'  => self::$map_selections['provider'],
318                    'styles'        => self::$map_providers[self::$map_selections['provider']]['styles'],
319                    'selectedStyle' => self::$map_selections['style'],
320                ];
321                break;
322            default:
323                $payload = null;
324        }
325
326        return $payload;
327    }
328
329    /**
330     * @param Request $request
331     * @param Tree    $tree
332     *
333     * @return object
334     */
335    public function getPedigreeMapAction(Request $request, Tree $tree)
336    {
337        $xref           = $request->get('xref', '');
338        $individual     = Individual::getInstance($xref, $tree);
339        $maxgenerations = $tree->getPreference('MAX_PEDIGREE_GENERATIONS');
340        $generations    = $request->get('generations', $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS'));
341
342        if ($individual === null) {
343            throw new IndividualNotFoundException();
344        }
345
346        if (!$individual->canShow()) {
347            throw new IndividualAccessDeniedException();
348        }
349
350        return $this->viewResponse('modules/pedigree-map/pedigree-map-page', [
351            'module'         => $this->getName(),
352            /* I18N: %s is an individual’s name */
353            'title'          => I18N::translate('Pedigree map of %s', $individual->getFullName()),
354            'tree'           => $tree,
355            'individual'     => $individual,
356            'generations'    => $generations,
357            'maxgenerations' => $maxgenerations,
358            'map'            => view(
359                'modules/pedigree-map/pedigree-map',
360                [
361                    'module'      => $this->getName(),
362                    'ref'         => $individual->xref(),
363                    'type'        => 'pedigree',
364                    'generations' => $generations,
365                ]
366            ),
367        ]);
368    }
369}
370